04Thanh search nhập vào header
Hàng ô tìm kiếm vốn nằm tách bên dưới header trượt lên và nhập chung vào một hàng với header chính khi bạn cuộn.
Dùng choHeader của dịch vụ tuyển dụng, bất động sản lấy tìm kiếm làm trung tâm
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">Nhập từ khóa tìm kiếm</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);
}
}// 실제 페이지: 스크롤하면 아래 Tìm kiếm 줄이 위로 미끄러져 헤더 한 줄 안으로 합쳐진다 (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 Thanh search nhập vào header — Diện tích đo được 15.596% · khung hình chuyển động 12/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · 9 mẫu css sticky header — copy-paste là chạy
Bài viết chứa demo này
9 mẫu css sticky header — copy-paste là chạyCss sticky header là cách giữ hàng menu trên cùng bám mãi trên màn hình dù cuộn xuống đâu, chỉ một dòng position: sticky Xem tất cả demo trong bài này


