spinner 900ms · skeleton 1.4s · dots stagger 120ms
Loading Patterns
待ち時間の見せ方 5 パターンを並べて比較する。spinner は安全策、skeleton は構造が見える分体感速度が速い。dots はチャット系で定番。
Spinner
linear rotate のみ OK
Progress (indeterminate)
長さ不明の処理
Skeleton
構造が見える分速く感じる
Shimmer
グラデーション sliding
Pulsing dots
チャット・思考中
/**
* Loading Patterns — 待たせるときの5パターン(Spinner / Progress / Skeleton / Shimmer / Pulsing dots)。
* 移植元: motion-playground app/_sections/LoadingPatterns.tsx(dark 変換 M-B-2)
*/
import { motion } from 'motion/react';
import type { ReactNode } from 'react';
function Spinner() {
return (
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 0.9, ease: 'linear', repeat: Infinity }}
className="h-8 w-8 rounded-full border-[3px] border-[var(--border-strong)] border-t-[var(--text-primary)]"
/>
);
}
function ProgressBar() {
return (
<div className="h-2 w-full overflow-hidden rounded-full bg-[var(--bg-elevated)]">
<motion.div
animate={{ x: ['-100%', '100%'] }}
transition={{
duration: 1.4,
ease: 'easeInOut',
repeat: Infinity,
}}
className="h-full w-1/2 rounded-full bg-[var(--text-primary)]"
/>
</div>
);
}
function Skeleton() {
return (
<div className="space-y-2">
{[80, 60, 90].map((w, i) => (
<motion.div
key={i}
animate={{ opacity: [0.4, 0.9, 0.4] }}
transition={{
duration: 1.4,
ease: 'easeInOut',
repeat: Infinity,
delay: i * 0.15,
}}
className="h-3 rounded-full bg-[var(--text-muted)]"
style={{ width: `${w}%` }}
/>
))}
</div>
);
}
function Shimmer() {
return (
<div className="relative h-16 w-full overflow-hidden rounded-md bg-[var(--bg-elevated)]">
<motion.div
animate={{ x: ['-100%', '200%'] }}
transition={{
duration: 1.6,
ease: 'easeInOut',
repeat: Infinity,
repeatDelay: 0.2,
}}
className="absolute inset-y-0 w-1/2 bg-gradient-to-r from-transparent via-[rgba(255,255,255,0.16)] to-transparent"
/>
</div>
);
}
function PulsingDots() {
return (
<div className="flex items-center gap-2">
{[0, 1, 2].map((i) => (
<motion.div
key={i}
animate={{ y: [0, -8, 0] }}
transition={{
duration: 0.7,
ease: 'easeInOut',
repeat: Infinity,
delay: i * 0.12,
}}
className="h-2.5 w-2.5 rounded-full bg-[var(--text-primary)]"
/>
))}
</div>
);
}
function Card({
title,
note,
children,
}: {
title: string;
note: string;
children: ReactNode;
}) {
return (
<div className="rounded-xl border border-[var(--border-subtle)] bg-[var(--bg-surface)] p-5">
<p className="text-sm font-medium text-[var(--text-primary)]">{title}</p>
<p className="mt-0.5 mb-4 text-xs text-[var(--text-muted)]">{note}</p>
<div className="flex h-20 items-center justify-center">{children}</div>
</div>
);
}
export default function LoadingPatterns() {
return (
<div
data-lab-island
className="grid w-full gap-3 p-4 sm:grid-cols-2 lg:grid-cols-3 lg:p-6"
>
<Card title="Spinner" note="linear rotate のみ OK">
<Spinner />
</Card>
<Card title="Progress (indeterminate)" note="長さ不明の処理">
<ProgressBar />
</Card>
<Card title="Skeleton" note="構造が見える分速く感じる">
<div className="w-full">
<Skeleton />
</div>
</Card>
<Card title="Shimmer" note="グラデーション sliding">
<Shimmer />
</Card>
<Card title="Pulsing dots" note="チャット・思考中">
<PulsingDots />
</Card>
</div>
);
} @keyframes loading-spin { to { transform: rotate(360deg); } }
@keyframes loading-progress { from { transform: translateX(-100%); } to { transform: translateX(100%); } }
@keyframes loading-pulse { 0%, 100% { opacity: 0.4; } 50% { opacity: 0.9; } }
@keyframes loading-dot { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-8px); } }
.loading-spinner {
width: 32px; height: 32px; border-radius: 999px;
border: 3px solid var(--border-strong);
border-top-color: var(--text-primary);
animation: loading-spin 0.9s linear infinite;
}
.loading-progress { height: 8px; border-radius: 999px; overflow: hidden; }
.loading-progress > span {
display: block; width: 50%; height: 100%;
background: var(--text-primary);
animation: loading-progress 1.4s ease-in-out infinite;
}
.loading-skeleton-bar { animation: loading-pulse 1.4s ease-in-out infinite; }
.loading-skeleton-bar:nth-child(2) { animation-delay: 0.15s; }
.loading-skeleton-bar:nth-child(3) { animation-delay: 0.3s; }
.loading-dot { animation: loading-dot 0.7s ease-in-out infinite; }
.loading-dot:nth-child(2) { animation-delay: 0.12s; }
.loading-dot:nth-child(3) { animation-delay: 0.24s; } Implement 5 loading indicators with motion/react (or pure CSS):
- spinner: ring with accent top border, rotate 0 -> 360deg, 900ms, linear, infinite
- progress: inner bar translateX -100% -> 100%, 1.4s, easeInOut, infinite
- skeleton: 3 bars, opacity 0.4 -> 0.9 -> 0.4, 1.4s, stagger 150ms
- shimmer: gradient band translateX -100% -> 200%, 1.6s + repeatDelay 200ms
- pulsing dots: translateY 0 -> -8 -> 0, 700ms, stagger 120ms
- all loop indefinitely; linear easing is allowed here (spinner / progress rule)