04検索バーの吸収
ヘッダー下に別行であった検索入力欄が、スクロールすると上へスライドしてメインヘッダーの1行に合流する。
使いどころ検索が中心の求人・不動産サービスのヘッダー
2행→1행translateY 합치기높이 트랜지션

<main class="stage">
<div class="sbm is-demo">
<div class="sbm__slot">
<header class="sbm__head">
<span class="sbm__logo">GODRICH</span>
<nav class="sbm__nav" aria-hidden="true"><span></span><span></span></nav>
</header>
<div class="sbm__search">
<span class="sbm__search-box">
<span class="sbm__search-icon" aria-hidden="true"></span>
<span class="sbm__search-text">検索キーワードを入力</span>
</span>
</div>
</div>
<div class="sbm__page">
<p class="sbm__line"></p>
<p class="sbm__line short"></p>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개. 변형은 이것만 바꾼다 ──────────────────────────
$duration: $dur-base; // 검색 줄이 위로 합쳐지는 시간
// 관성 lerp — p += (목표-p) * .32 를 20스텝 반복한 계산 결과를 커브로 굽는다 (형태 2차, 사용자 2026-09-14)
$easing: linear(0, .32, .538, .686, .786, .855, .901, .933, .954, .969, .979, .986, .99, .993, .995, .997, .998, .999, .999, .999, 1);
$easing-content: linear(0, .18, .328, .449, .548, .629, .696, .751, .796, .832, .863, .887, .908, .924, .938, .949, .958, .966, .972, .977, 1); // 알파 .18 — 헤더보다 느리게 따라잡아 시차가 생긴다
$dur-content: $dur-base * 1.4;
$color: $subject-blue; // 검색 아이콘·포커스 색
@include stage($stage-paper);
$row1: 56px;
$row2: 40px;
.sbm { width: min(380px, 90vw); }
// ── 고정 높이 슬롯: 헤더 줄 + 검색 줄 두 층을 겹쳐 두고 transform 만 움직인다 ──
.sbm__slot {
position: relative;
height: $row1 + $row2;
}
.sbm__head {
position: absolute; inset: 0 0 auto 0;
z-index: 2;
height: $row1;
display: flex; align-items: center; justify-content: space-between;
padding: 0 $sp-4;
}
.sbm__logo { color: $stage-ink; font-weight: 800; font-size: clamp(14px, 4vw, 18px); line-height: 1; font-family: inherit; letter-spacing: .04em; }
.sbm__nav {
display: flex; gap: $sp-2;
transition: opacity $duration $easing, transform $duration $easing;
span { width: 20px; height: 6px; border-radius: $r-pill; background: rgba($stage-ink, .2); }
}
// 검색 줄은 헤더의 nav 자리(오른쪽)와 같은 정렬로 둔다 — 위로 합쳐져도 로고와 겹치지 않는다
.sbm__search {
position: absolute; left: 0; right: 0; top: $row1;
z-index: 1;
height: $row2;
display: flex; align-items: center; justify-content: flex-end;
padding: 0 $sp-4;
border-top: 1px solid rgba($stage-ink, .08);
transform: translateY(0);
transition: transform $duration $easing;
&-box {
display: flex; align-items: center; gap: $sp-2;
padding: $sp-1 $sp-3;
border-radius: $r-pill;
background: rgba($color, .1);
}
&-icon { flex: none; width: 14px; height: 14px; border-radius: 50%; border: 2px solid $color; }
&-text { font-weight: 500; font-size: clamp(11px, 3vw, 12px); line-height: 1; font-family: inherit; color: rgba($stage-ink, .72); }
}
// 실제 사용: 스크롤을 시작하면 검색 줄이 위로 미끄러져 헤더 줄 안으로 합쳐진다
.sbm.is-scrolled {
.sbm__nav { opacity: 0; transform: translateX($sp-2); }
.sbm__search { transform: translateY(-#{$row1}); }
// 콘텐츠는 헤더 합쳐짐보다 늦게 도착 — 시차 스크롤
.sbm__page { transform: translateY(-8px); }
}
// ── 데모(.is-demo): 합쳐졌다 돌아오는 2초 루프 ──────────────
.sbm.is-demo {
.sbm__nav { animation: sbm-nav-loop $dur-loop $easing infinite; }
.sbm__search { animation: sbm-search-loop $dur-loop $easing infinite; }
.sbm__page { animation: sbm-page-loop $dur-loop $easing-content infinite; }
}
@keyframes sbm-nav-loop {
0%, 20% { opacity: 1; transform: translateX(0); }
45%, 70% { opacity: 0; transform: translateX(8px); }
95%, 100% { opacity: 1; transform: translateX(0); }
}
@keyframes sbm-search-loop {
0%, 20% { transform: translateY(0); }
45%, 70% { transform: translateY(-#{$row1}); }
95%, 100% { transform: translateY(0); }
}
// 헤더보다 느린 $easing-content 로 같은 구간을 밟는다 — 관성이 덜 붙어 뒤처져 보인다
@keyframes sbm-page-loop {
0%, 20% { transform: translateY(0); }
45%, 70% { transform: translateY(-8px); }
95%, 100% { transform: translateY(0); }
}
// ── 배경: 문서처럼 보이는 자리 ─────────────────────────
.sbm__page {
padding: $sp-4 0 0;
transform: translateY(0);
transition: transform $dur-content $easing-content;
}
.sbm__line {
height: 10px;
margin: 0 0 $sp-3;
border-radius: $r-xs;
background: rgba($stage-ink, .08);
&.short { width: 55%; }
}
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.sbm__nav, .sbm__search, .sbm__page { transition: none; }
.sbm.is-demo {
.sbm__nav, .sbm__search, .sbm__page { animation: none !important; }
.sbm__nav { opacity: 1; transform: translateX(0); }
.sbm__search { transform: translateY(0); }
.sbm__page { transform: translateY(0); }
}
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.sbm {
width: min(380px, 90vw);
}
.sbm__slot {
position: relative;
height: 96px;
}
.sbm__head {
position: absolute;
inset: 0 0 auto 0;
z-index: 2;
height: 56px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
}
.sbm__logo {
color: #17141a;
font-weight: 800;
font-size: clamp(14px, 4vw, 18px);
line-height: 1;
font-family: inherit;
letter-spacing: 0.04em;
}
.sbm__nav {
display: flex;
gap: 8px;
transition: opacity 300ms linear(0, 0.32, 0.538, 0.686, 0.786, 0.855, 0.901, 0.933, 0.954, 0.969, 0.979, 0.986, 0.99, 0.993, 0.995, 0.997, 0.998, 0.999, 0.999, 0.999, 1), transform 300ms linear(0, 0.32, 0.538, 0.686, 0.786, 0.855, 0.901, 0.933, 0.954, 0.969, 0.979, 0.986, 0.99, 0.993, 0.995, 0.997, 0.998, 0.999, 0.999, 0.999, 1);
}
.sbm__nav span {
width: 20px;
height: 6px;
border-radius: 999px;
background: rgba(23, 20, 26, 0.2);
}
.sbm__search {
position: absolute;
left: 0;
right: 0;
top: 56px;
z-index: 1;
height: 40px;
display: flex;
align-items: center;
justify-content: flex-end;
padding: 0 16px;
border-top: 1px solid rgba(23, 20, 26, 0.08);
transform: translateY(0);
transition: transform 300ms linear(0, 0.32, 0.538, 0.686, 0.786, 0.855, 0.901, 0.933, 0.954, 0.969, 0.979, 0.986, 0.99, 0.993, 0.995, 0.997, 0.998, 0.999, 0.999, 0.999, 1);
}
.sbm__search-box {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 12px;
border-radius: 999px;
background: rgba(47, 109, 246, 0.1);
}
.sbm__search-icon {
flex: none;
width: 14px;
height: 14px;
border-radius: 50%;
border: 2px solid #2f6df6;
}
.sbm__search-text {
font-weight: 500;
font-size: clamp(11px, 3vw, 12px);
line-height: 1;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
}
.sbm.is-scrolled .sbm__nav {
opacity: 0;
transform: translateX(8px);
}
.sbm.is-scrolled .sbm__search {
transform: translateY(-56px);
}
.sbm.is-scrolled .sbm__page {
transform: translateY(-8px);
}
.sbm.is-demo .sbm__nav {
animation: sbm-nav-loop 2s linear(0, 0.32, 0.538, 0.686, 0.786, 0.855, 0.901, 0.933, 0.954, 0.969, 0.979, 0.986, 0.99, 0.993, 0.995, 0.997, 0.998, 0.999, 0.999, 0.999, 1) infinite;
}
.sbm.is-demo .sbm__search {
animation: sbm-search-loop 2s linear(0, 0.32, 0.538, 0.686, 0.786, 0.855, 0.901, 0.933, 0.954, 0.969, 0.979, 0.986, 0.99, 0.993, 0.995, 0.997, 0.998, 0.999, 0.999, 0.999, 1) infinite;
}
.sbm.is-demo .sbm__page {
animation: sbm-page-loop 2s linear(0, 0.18, 0.328, 0.449, 0.548, 0.629, 0.696, 0.751, 0.796, 0.832, 0.863, 0.887, 0.908, 0.924, 0.938, 0.949, 0.958, 0.966, 0.972, 0.977, 1) infinite;
}
@keyframes sbm-nav-loop {
0%, 20% {
opacity: 1;
transform: translateX(0);
}
45%, 70% {
opacity: 0;
transform: translateX(8px);
}
95%, 100% {
opacity: 1;
transform: translateX(0);
}
}
@keyframes sbm-search-loop {
0%, 20% {
transform: translateY(0);
}
45%, 70% {
transform: translateY(-56px);
}
95%, 100% {
transform: translateY(0);
}
}
@keyframes sbm-page-loop {
0%, 20% {
transform: translateY(0);
}
45%, 70% {
transform: translateY(-8px);
}
95%, 100% {
transform: translateY(0);
}
}
.sbm__page {
padding: 16px 0 0;
transform: translateY(0);
transition: transform 420ms linear(0, 0.18, 0.328, 0.449, 0.548, 0.629, 0.696, 0.751, 0.796, 0.832, 0.863, 0.887, 0.908, 0.924, 0.938, 0.949, 0.958, 0.966, 0.972, 0.977, 1);
}
.sbm__line {
height: 10px;
margin: 0 0 12px;
border-radius: 4px;
background: rgba(23, 20, 26, 0.08);
}
.sbm__line.short {
width: 55%;
}
@media (prefers-reduced-motion: reduce) {
.sbm__nav, .sbm__search, .sbm__page {
transition: none;
}
.sbm.is-demo .sbm__nav, .sbm.is-demo .sbm__search, .sbm.is-demo .sbm__page {
animation: none !important;
}
.sbm.is-demo .sbm__nav {
opacity: 1;
transform: translateX(0);
}
.sbm.is-demo .sbm__search {
transform: translateY(0);
}
.sbm.is-demo .sbm__page {
transform: translateY(0);
}
}// 실제 페이지: 스크롤하면 아래 検索 줄이 위로 미끄러져 헤더 한 줄 안으로 합쳐진다 (7줄)
var el = document.querySelector('.sbm');
window.addEventListener('scroll', function () {
el.classList.toggle('is-scrolled', window.scrollY > 20);
}, { passive: true });import { useEffect, useRef } from "react";
import type { CSSProperties, ReactNode } from "react";
import styles from "./SearchBarMerge.module.scss";
export type SearchBarMergeProps = {
logo: string;
placeholder: string;
children?: ReactNode;
/** 검색 줄이 위로 합쳐지는 시간 (기본 300ms) */
duration?: number;
/** 관성 lerp 커브(p += (목표-p)*.32 를 20스텝 반복해 구운 linear()) — 기본값이 vanilla 와 같다 */
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const LERP_EASING = "linear(0, .32, .538, .686, .786, .855, .901, .933, .954, .969, .979, .986, .99, .993, .995, .997, .998, .999, .999, .999, 1)";
export default function SearchBarMerge({
logo,
placeholder,
children,
duration = 300,
easing = LERP_EASING,
color = "#2f6df6",
}: SearchBarMergeProps) {
const ref = useRef<HTMLDivElement>(null);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
useEffect(() => {
function onScroll() {
ref.current?.classList.toggle("is-scrolled", window.scrollY > 20);
}
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
return (
<div ref={ref} className={styles.slot} style={vars}>
<header className={styles.head}>
<span className={styles.logo}>{logo}</span>
<nav className={styles.nav}>{children}</nav>
</header>
<div className={styles.search}>
<span className={styles.searchBox}>
<span className={styles.searchIcon} />
<span className={styles.searchText}>{placeholder}</span>
</span>
</div>
</div>
);
}04 検索バーの吸収 — 計測 累積面積 15.596% · 動いたフレーム 12/23GODRICH オリジナル · SCSS と React は zip に同梱 · CSS スティッキーヘッダー追加9選 — 検索バー・CTA・通知まで
このデモが属する記事
CSS スティッキーヘッダー追加9選 — 検索バー・CTA・通知までcss スティッキーヘッダーとは、スクロールしてもページ上部のメニュー行が画面に貼り付いたまま残る機能で、position: stickyを一行書くだけで実現できます。ここで紹介する9個は貼り付くだけのヘッダーの一歩先で、スクロールする間に この記事のデモをすべて見る


