02Header nhóm cố định
Danh bạ được nhóm theo chữ cái đầu, header của nhóm hiện tại dính trên cùng khi cuộn cho đến khi nhóm kế tiếp đẩy nó lên.
Dùng choDanh sách danh bạ, danh sách sản phẩm theo danh mục
position:sticky top:0그룹별 wrapper파스텔 톤

<main class="stage">
<div class="grp-wrap is-demo" tabindex="0" aria-label="Danh sách liên hệ. Cuộn bằng phím mũi tên hoặc kéo">
<ul class="grp-list">
<li class="grp-group">
<div class="grp-group__header">A</div>
<ul class="grp-group__rows">
<li>Ari</li><li>Aiden</li><li>Ava</li>
</ul>
</li>
<li class="grp-group">
<div class="grp-group__header">B</div>
<ul class="grp-group__rows">
<li>Bella</li><li>Ben</li><li>Bora</li>
</ul>
</li>
<li class="grp-group">
<div class="grp-group__header">C</div>
<ul class="grp-group__rows">
<li>Chloe</li><li>Charlie</li><li>Cyan</li>
</ul>
</li>
</ul>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-base; // 스크롤 자동 시연 왕복 시간(스크립트 값과 맞춤)
$easing: $ease-out;
$color: $subject-pink; // 그룹 헤더 배경 (파스텔)
@include stage($stage-paper);
.grp-wrap {
width: 220px;
height: 168px;
overflow: auto;
border-radius: $r-card;
background: $stage-paper;
box-shadow: $shadow-raised;
}
.grp-wrap:focus-visible { outline: 2px solid $subject-blue; outline-offset: 2px; }
.grp-list { list-style: none; margin: 0; padding: 0; }
.grp-group { margin: 0; padding: 0; }
.grp-group__header {
position: sticky;
top: 0;
z-index: 1;
padding: $sp-2 $sp-4;
font-weight: 700;
font-size: 13px;
color: $stage-ink;
background: $color;
}
.grp-group:nth-of-type(2) .grp-group__header { background: $subject-mint; }
.grp-group:nth-of-type(3) .grp-group__header { background: $subject-lilac; }
.grp-group__rows {
list-style: none;
margin: 0;
padding: 0;
}
.grp-group__rows li {
padding: $sp-2 $sp-4;
font-size: 13px;
color: $stage-ink;
border-bottom: 1px solid rgba(23, 20, 26, .06);
}
@media (prefers-reduced-motion: reduce) {
.grp-wrap { scroll-behavior: auto; }
}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);
}
.grp-wrap {
width: 220px;
height: 168px;
overflow: auto;
border-radius: 16px;
background: #ffffff;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
}
.grp-wrap:focus-visible {
outline: 2px solid #2f6df6;
outline-offset: 2px;
}
.grp-list {
list-style: none;
margin: 0;
padding: 0;
}
.grp-group {
margin: 0;
padding: 0;
}
.grp-group__header {
position: sticky;
top: 0;
z-index: 1;
padding: 8px 16px;
font-weight: 700;
font-size: 13px;
color: #17141a;
background: #ff9ebb;
}
.grp-group:nth-of-type(2) .grp-group__header {
background: #4cd4a6;
}
.grp-group:nth-of-type(3) .grp-group__header {
background: #b9a0ff;
}
.grp-group__rows {
list-style: none;
margin: 0;
padding: 0;
}
.grp-group__rows li {
padding: 8px 16px;
font-size: 13px;
color: #17141a;
border-bottom: 1px solid rgba(23, 20, 26, 0.06);
}
@media (prefers-reduced-motion: reduce) {
.grp-wrap {
scroll-behavior: auto;
}
}(function () {
var wrap = document.querySelector('.grp-wrap');
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var raf = null, t0 = null;
function maxY() { return wrap.scrollHeight - wrap.clientHeight; }
function loop(ts) {
if (!wrap.classList.contains('is-demo')) return;
if (t0 === null) t0 = ts;
var p = ((ts - t0) / 2000) % 1;
var s = p < .5 ? p * 2 : 2 - p * 2;
wrap.scrollTop = s * maxY();
raf = requestAnimationFrame(loop);
}
function stop() {
wrap.classList.remove('is-demo');
if (raf) cancelAnimationFrame(raf);
}
wrap.addEventListener('pointerdown', stop);
wrap.addEventListener('wheel', stop, { passive: true });
wrap.addEventListener('keydown', function (e) {
var step = 24;
if (e.key === 'ArrowDown') { stop(); wrap.scrollTop += step; }
else if (e.key === 'ArrowUp') { stop(); wrap.scrollTop -= step; }
else return;
e.preventDefault();
});
if (!reduce) requestAnimationFrame(loop);
})();import type { CSSProperties } from "react";
import styles from "./GroupedHeaderStickySection.module.scss";
export type Group = { letter: string; names: string[] };
export type GroupedHeaderStickySectionProps = {
groups: Group[];
/** 스크롤 이징 (기본 300ms) */
duration?: number;
easing?: string;
/** 첫 그룹 헤더 배경(파스텔) */
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
/** 그룹 헤더가 position:sticky 로 스크롤 중 위에 붙어 있다가 다음 그룹이 밀어 올린다 */
export default function GroupedHeaderStickySection({
groups,
duration = 300,
easing = "ease-out",
color = "#ff9ebb",
}: GroupedHeaderStickySectionProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.wrap} style={vars} tabIndex={0} aria-label="연락처 목록">
<ul className={styles.list}>
{groups.map((g, i) => (
<li key={g.letter} className={styles.group}>
<div className={styles.header} data-i={i % 3}>{g.letter}</div>
<ul className={styles.rows}>
{g.names.map((n) => <li key={n}>{n}</li>)}
</ul>
</li>
))}
</ul>
</div>
);
}02 Header nhóm cố định — Diện tích đo được 25.533% · khung hình chuyển động 23/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · 9 mẫu List UI: accordion list css và checklist
Bài viết chứa demo này
9 mẫu List UI: accordion list css và checklistAccordion list css là kiểu danh sách mà khi bạn bấm vào một dòng, chiều cao của nó tự giãn ra để lộ nội dung chi tiết — Xem tất cả demo trong bài này
Nếu bạn thích hiệu ứng này - demo tương tự
Giao diện danh sách · bảng06 Cố định header + cột đầu9 mẫu List UI: swipe to delete css và phân trang
Giao diện nhập liệu01 Tự chuyển sang ô kế tiếpOTP Input UI: 9 tình huống kèm validation
Giao diện đăng nhập · xác thực01 Thẻ đăng nhập kiểu kính mờ (glassmorphism)9 mẫu form đăng ký & đăng nhập copy-paste được