07スケルトンローディング行
データが来る前、グレーのバー行が左右に光を流し、コンテンツが入る場所を先に見せる。
使いどころフィードの初期ロード、検索結果の待機画面
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>イ・ソジュン</strong><span>新しいコメントを残しました</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>パク・ハユン</strong><span>投稿をシェアしました</span></span>
</div>
<button class="skel-list__toggle" type="button">コンテンツをプレビュー</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 ? 'スケルトンをもう一度見る' : 'コンテンツをプレビュー';
});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 スケルトンローディング行 — 計測 累積面積 4.918% · 動いたフレーム 22/23GODRICH オリジナル · SCSS と React は zip に同梱 · スワイプ削除リストUI 9選 — ページネーションまで
このデモが属する記事
スワイプ削除リストUI 9選 — ページネーションまでスワイプ削除とは、リストの行を横に押すと裏に隠れていた削除ボタンが現れる動きのことで、アプリで見たあの操作をウェブでもそのまま使いたい人は多いです。この記事では、そのスワイプ削除を含むリストのインタラクションを9個、実際に動くコードでまとめ この記事のデモをすべて見る


