07Danh sách skeleton shimmer
Trước khi có dữ liệu, các hàng thanh xám lướt ánh sáng qua lại, báo trước vị trí nội dung sẽ hiện.
Dùng choTải feed lần đầu, màn hình chờ kết quả tìm kiếm
shimmer gradient sweeparia-busybackground-position keyframes

<main class="stage">
<div class="skel-list" aria-busy="true">
<div class="skel-list__row">
<span class="skel-list__avatar" aria-hidden="true"></span>
<span class="skel-list__lines" aria-hidden="true"><span></span><span></span></span>
<span class="skel-list__content"><strong>Lee Seo-jun</strong><span>đã để lại bình luận mới</span></span>
</div>
<div class="skel-list__row">
<span class="skel-list__avatar" aria-hidden="true"></span>
<span class="skel-list__lines" aria-hidden="true"><span></span><span></span></span>
<span class="skel-list__content"><strong>Park Ha-yoon</strong><span>đã chia sẻ bài viết</span></span>
</div>
<button class="skel-list__toggle" type="button">Xem trước nội dung</button>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: 1.4s; // 한 번 훑는 시간
$easing: linear;
$color: #d8d8de; // 스켈레톤 막대 바탕
$highlight: rgba(255, 255, 255, .95); // 훑고 지나가는 밝은 띠 — 바탕과 대비를 크게 둔다
@include stage($stage-orange);
.skel-list {
width: min(240px, 78vw);
display: flex;
flex-direction: column;
gap: $sp-3;
}
.skel-list__row {
display: flex;
align-items: center;
gap: $sp-3;
padding: $sp-3;
border-radius: $r-card;
background: $stage-paper;
}
.skel-list__avatar {
flex: 0 0 auto;
width: 32px;
height: 32px;
border-radius: 50%;
background: linear-gradient(90deg, $color 25%, $highlight 50%, $color 75%);
background-size: 200% 100%;
animation: skel-shimmer $duration $easing infinite;
}
.skel-list__lines {
flex: 1 1 auto;
display: flex;
flex-direction: column;
gap: $sp-2;
span {
display: block;
height: 10px;
border-radius: $r-xs;
background: linear-gradient(90deg, $color 25%, $highlight 50%, $color 75%);
background-size: 200% 100%;
animation: skel-shimmer $duration $easing infinite;
&:last-child { width: 60%; }
}
}
.skel-list__content {
display: none;
flex: 1 1 auto;
flex-direction: column;
gap: $sp-1;
font-size: 12px;
strong { font-size: 13px; color: $stage-ink; }
span { color: rgba(23, 20, 26, .6); }
}
.skel-list.is-loaded .skel-list__avatar,
.skel-list.is-loaded .skel-list__lines { display: none; }
.skel-list.is-loaded .skel-list__content { display: flex; }
.skel-list__toggle {
align-self: flex-start;
border: 0;
border-radius: $r-pill;
padding: $sp-1 $sp-3;
background: $stage-ink;
color: $stage-paper;
font-size: 11px;
font-weight: 700;
font-family: inherit;
cursor: pointer;
}
@keyframes skel-shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
@media (prefers-reduced-motion: reduce) {
.skel-list__avatar, .skel-list__lines span { animation-duration: 3.2s; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.skel-list {
width: min(240px, 78vw);
display: flex;
flex-direction: column;
gap: 12px;
}
.skel-list__row {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border-radius: 16px;
background: #ffffff;
}
.skel-list__avatar {
flex: 0 0 auto;
width: 32px;
height: 32px;
border-radius: 50%;
background: linear-gradient(90deg, #d8d8de 25%, rgba(255, 255, 255, 0.95) 50%, #d8d8de 75%);
background-size: 200% 100%;
animation: skel-shimmer 1.4s linear infinite;
}
.skel-list__lines {
flex: 1 1 auto;
display: flex;
flex-direction: column;
gap: 8px;
}
.skel-list__lines span {
display: block;
height: 10px;
border-radius: 4px;
background: linear-gradient(90deg, #d8d8de 25%, rgba(255, 255, 255, 0.95) 50%, #d8d8de 75%);
background-size: 200% 100%;
animation: skel-shimmer 1.4s linear infinite;
}
.skel-list__lines span:last-child {
width: 60%;
}
.skel-list__content {
display: none;
flex: 1 1 auto;
flex-direction: column;
gap: 4px;
font-size: 12px;
}
.skel-list__content strong {
font-size: 13px;
color: #17141a;
}
.skel-list__content span {
color: rgba(23, 20, 26, 0.6);
}
.skel-list.is-loaded .skel-list__avatar,
.skel-list.is-loaded .skel-list__lines {
display: none;
}
.skel-list.is-loaded .skel-list__content {
display: flex;
}
.skel-list__toggle {
align-self: flex-start;
border: 0;
border-radius: 999px;
padding: 4px 12px;
background: #17141a;
color: #ffffff;
font-size: 11px;
font-weight: 700;
font-family: inherit;
cursor: pointer;
}
@keyframes skel-shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
@media (prefers-reduced-motion: reduce) {
.skel-list__avatar, .skel-list__lines span {
animation-duration: 3.2s;
}
}document.querySelector('.skel-list__toggle').addEventListener('click', function () {
var list = document.querySelector('.skel-list');
var loaded = list.classList.toggle('is-loaded');
list.setAttribute('aria-busy', String(!loaded));
this.textContent = loaded ? 'Xem lại skeleton' : 'Xem trước nội dung';
});import { useState, type CSSProperties } from "react";
import styles from "./SkeletonShimmerRows.module.scss";
export type SkeletonShimmerRowsProps = {
people: { name: string; action: string }[];
loaded?: boolean;
/** 한 번 훑는 시간 (기본 1.4s) */
duration?: number;
};
type Vars = CSSProperties & { "--duration"?: string };
export default function SkeletonShimmerRows({ people, loaded = false, duration = 1400 }: SkeletonShimmerRowsProps) {
const [isLoaded, setIsLoaded] = useState(loaded);
const vars: Vars = { "--duration": `${duration}ms` };
return (
<div className={styles.list} style={vars} aria-busy={!isLoaded}>
{people.map((p) => (
<div className={styles.row} key={p.name}>
{isLoaded ? (
<span className={styles.content}>
<strong>{p.name}</strong>
<span>{p.action}</span>
</span>
) : (
<>
<span className={styles.avatar} aria-hidden="true" />
<span className={styles.lines} aria-hidden="true"><span /><span /></span>
</>
)}
</div>
))}
<button className={styles.toggle} type="button" onClick={() => setIsLoaded((v) => !v)}>
{isLoaded ? "스켈레톤 다시 보기" : "콘텐츠 미리보기"}
</button>
</div>
);
}07 Danh sách skeleton shimmer — Diện tích đo được 4.918% · khung hình chuyển động 22/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · 9 mẫu List UI: swipe to delete css và phân trang
Bài viết chứa demo này
9 mẫu List UI: swipe to delete css và phân trangSwipe to delete css là vuốt một dòng để lộ nút xóa phía sau — thao tác bạn hay thấy trong app, giờ có luôn trên web. Bài 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ảng03 Chuyển slide từ list sang chi tiết9 mẫu List UI: accordion list css và checklist
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