Dashboard Skeleton
Original · freedashboard widgets skeleton
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/skel-dashboard.json"use client";
import { useId, useRef, useState } from "react";
import type { CSSProperties, KeyboardEvent } from "react";
import { motion, useReducedMotion } from "motion/react";
type Effect = "shimmer" | "pulse" | "wave";
type SkdStyle = CSSProperties & Record<`--${string}`, string | number>;
const EFFECTS: ReadonlyArray<{ id: Effect; label: string }> = [
{ id: "shimmer", label: "Shimmer" },
{ id: "pulse", label: "Pulse" },
{ id: "wave", label: "Wave" },
];
const BAR_HEIGHTS = [42, 68, 54, 83, 60, 38, 74, 91, 49, 66, 79, 57];
const CARD =
"rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900";
function delayStyle(ms: number, extra?: CSSProperties): SkdStyle {
return { ...extra, "--skd-delay": `${ms}ms` };
}
function Block({
className = "",
style,
}: {
className?: string;
style?: SkdStyle;
}) {
return (
<div
aria-hidden="true"
style={style}
className={`skd-b relative overflow-hidden bg-slate-200/80 dark:bg-slate-700/60 ${className}`}
/>
);
}
export default function SkelDashboard() {
const reduce = useReducedMotion();
const [effect, setEffect] = useState<Effect>("shimmer");
const [pace, setPace] = useState(1.4);
const [compact, setCompact] = useState(false);
const [replayKey, setReplayKey] = useState(0);
const [status, setStatus] = useState("Loading dashboard widgets.");
const paceId = useId();
const radioRefs = useRef<Array<HTMLButtonElement | null>>([]);
const rows = compact ? 4 : 6;
const pad = compact ? "p-4" : "p-5";
const chartPad = compact ? "p-4" : "p-6";
function onRadioKey(e: KeyboardEvent<HTMLDivElement>) {
const idx = EFFECTS.findIndex((x) => x.id === effect);
let next = idx;
if (e.key === "ArrowRight" || e.key === "ArrowDown") next = (idx + 1) % EFFECTS.length;
else if (e.key === "ArrowLeft" || e.key === "ArrowUp") next = (idx - 1 + EFFECTS.length) % EFFECTS.length;
else if (e.key === "Home") next = 0;
else if (e.key === "End") next = EFFECTS.length - 1;
else return;
e.preventDefault();
setEffect(EFFECTS[next].id);
radioRefs.current[next]?.focus();
}
function replay() {
setReplayKey((k) => k + 1);
setStatus(`Reloading widgets at ${pace.toFixed(1)}s cycle.`);
}
const ring =
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:focus-visible:ring-offset-slate-950";
return (
<section className="relative w-full overflow-hidden bg-gradient-to-b from-slate-50 to-white px-4 py-16 sm:px-6 sm:py-20 lg:px-8 dark:from-slate-950 dark:to-slate-900">
<style>{`
[data-skd]{ --skd-hl: rgba(255,255,255,0.62); }
@media (prefers-color-scheme: dark){ [data-skd]{ --skd-hl: rgba(255,255,255,0.09); } }
:root[data-theme="dark"] [data-skd], .dark [data-skd]{ --skd-hl: rgba(255,255,255,0.09); }
:root[data-theme="light"] [data-skd]{ --skd-hl: rgba(255,255,255,0.62); }
[data-skd-effect="shimmer"] .skd-b::after,
[data-skd-effect="wave"] .skd-b::after{
content:"";
position:absolute;
inset:0;
background:linear-gradient(90deg, transparent 0%, var(--skd-hl) 50%, transparent 100%);
background-size:200% 100%;
background-repeat:no-repeat;
animation: skd-dash-sweep var(--skd-dur,1.4s) linear infinite;
}
[data-skd-effect="wave"] .skd-b::after{ animation-delay: var(--skd-delay, 0ms); }
[data-skd-effect="pulse"] .skd-b{
animation: skd-dash-pulse var(--skd-dur,1.4s) ease-in-out infinite;
}
@keyframes skd-dash-sweep{
0%{ background-position:-150% 0; }
100%{ background-position:150% 0; }
}
@keyframes skd-dash-pulse{
0%,100%{ opacity:1; }
50%{ opacity:.45; }
}
@media (prefers-reduced-motion: reduce){
[data-skd] .skd-b, [data-skd] .skd-b::after{ animation: none !important; }
}
`}</style>
<div
aria-hidden="true"
className="pointer-events-none absolute -top-24 right-0 h-72 w-72 rounded-full bg-indigo-300/25 blur-3xl dark:bg-indigo-500/10"
/>
<div className="relative mx-auto max-w-6xl">
<p className="sr-only" role="status" aria-live="polite">
{status}
</p>
<motion.header
initial={reduce ? false : { opacity: 0, y: -10 }}
animate={reduce ? undefined : { opacity: 1, y: 0 }}
transition={{ duration: 0.4, ease: "easeOut" }}
className="flex flex-col gap-6 md:flex-row md:items-end md:justify-between"
>
<div>
<div className="flex items-center gap-2.5">
<span className="inline-flex items-center gap-1.5 rounded-full bg-amber-100 px-2.5 py-1 text-xs font-medium text-amber-800 dark:bg-amber-400/10 dark:text-amber-300">
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-amber-500 motion-reduce:animate-none dark:bg-amber-400" />
Live sync
</span>
<span className="text-xs font-medium text-slate-400 dark:text-slate-500">
Updated moments ago
</span>
</div>
<h2 className="mt-3 text-2xl font-semibold tracking-tight text-slate-900 sm:text-3xl dark:text-white">
Analytics overview
</h2>
<p className="mt-1.5 text-sm text-slate-500 dark:text-slate-400">
Fetching your latest metrics. Widgets fill in as data streams from each source.
</p>
</div>
<div className={`${CARD} flex flex-wrap items-center gap-4 p-3`}>
<div className="flex flex-col gap-1.5">
<span className="px-1 text-[0.65rem] font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">
Effect
</span>
<div
role="radiogroup"
aria-label="Skeleton animation effect"
onKeyDown={onRadioKey}
className="inline-flex rounded-full bg-slate-100 p-1 dark:bg-slate-800"
>
{EFFECTS.map((opt, i) => {
const selected = effect === opt.id;
return (
<button
key={opt.id}
type="button"
role="radio"
aria-checked={selected}
tabIndex={selected ? 0 : -1}
ref={(el) => {
radioRefs.current[i] = el;
}}
onClick={() => setEffect(opt.id)}
className={`rounded-full px-3 py-1 text-sm font-medium transition-colors ${ring} ${
selected
? "bg-white text-slate-900 shadow-sm dark:bg-slate-600 dark:text-white"
: "text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-200"
}`}
>
{opt.label}
</button>
);
})}
</div>
</div>
<div className="flex flex-col gap-1.5">
<label
htmlFor={paceId}
className="px-1 text-[0.65rem] font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400"
>
Pace {pace.toFixed(1)}s
</label>
<input
id={paceId}
type="range"
min={0.6}
max={3}
step={0.1}
value={pace}
onChange={(e) => setPace(Number.parseFloat(e.target.value))}
aria-valuetext={`${pace.toFixed(1)} seconds per cycle`}
className={`h-9 w-28 cursor-pointer accent-indigo-600 dark:accent-indigo-400 ${ring} rounded-full`}
/>
</div>
<div className="flex flex-col gap-1.5">
<span className="px-1 text-[0.65rem] font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">
Density
</span>
<button
type="button"
role="switch"
aria-checked={compact}
onClick={() => setCompact((v) => !v)}
className={`group inline-flex h-9 items-center gap-2 rounded-full px-1 ${ring}`}
>
<span className="text-sm font-medium text-slate-600 dark:text-slate-300">
Compact
</span>
<span
className={`relative h-6 w-11 rounded-full transition-colors ${
compact ? "bg-indigo-600" : "bg-slate-300 dark:bg-slate-700"
}`}
>
<span
className={`absolute left-0.5 top-0.5 h-5 w-5 rounded-full bg-white shadow transition-transform ${
compact ? "translate-x-5" : "translate-x-0"
}`}
/>
</span>
</button>
</div>
<div className="flex flex-col gap-1.5">
<span className="px-1 text-[0.65rem] font-semibold uppercase tracking-wider text-transparent">
.
</span>
<button
type="button"
onClick={replay}
className={`inline-flex h-9 items-center gap-2 rounded-full bg-indigo-600 px-4 text-sm font-semibold text-white transition-colors hover:bg-indigo-500 ${ring}`}
>
<svg
viewBox="0 0 24 24"
aria-hidden="true"
className="h-4 w-4"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M3 12a9 9 0 0 1 15.5-6.3L21 8" />
<path d="M21 3v5h-5" />
<path d="M21 12a9 9 0 0 1-15.5 6.3L3 16" />
<path d="M3 21v-5h5" />
</svg>
Replay
</button>
</div>
</div>
</motion.header>
<div
key={replayKey}
data-skd=""
data-skd-effect={effect}
aria-busy="true"
style={{ "--skd-dur": `${pace}s` } as SkdStyle}
className={`mt-8 ${compact ? "space-y-4" : "space-y-6"}`}
>
<span className="sr-only">Loading dashboard widgets, please wait.</span>
{/* KPI stat cards */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{[0, 1, 2, 3].map((i) => (
<div key={i} className={`${CARD} ${pad}`}>
<div className="flex items-center justify-between">
<Block className="h-10 w-10 rounded-xl" style={delayStyle(i * 90)} />
<Block className="h-5 w-14 rounded-full" style={delayStyle(i * 90 + 40)} />
</div>
<Block className="mt-4 h-3 w-24 rounded-full" style={delayStyle(i * 90 + 80)} />
<Block className="mt-3 h-7 w-28 rounded-lg" style={delayStyle(i * 90 + 120)} />
<Block className="mt-3 h-3 w-20 rounded-full" style={delayStyle(i * 90 + 160)} />
</div>
))}
</div>
{/* Chart + breakdown */}
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
<div className={`${CARD} ${chartPad} lg:col-span-2`}>
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="space-y-2">
<Block className="h-4 w-40 rounded-full" style={delayStyle(0)} />
<Block className="h-3 w-28 rounded-full" style={delayStyle(60)} />
</div>
<div className="flex items-center gap-3">
{[0, 1, 2].map((i) => (
<div key={i} className="flex items-center gap-1.5">
<Block className="h-2.5 w-2.5 rounded-sm" style={delayStyle(80 + i * 40)} />
<Block className="h-3 w-12 rounded-full" style={delayStyle(100 + i * 40)} />
</div>
))}
</div>
</div>
<div className="mt-6 flex h-48 items-end gap-2">
{BAR_HEIGHTS.map((h, i) => (
<div key={i} className="flex h-full flex-1 items-end">
<Block
className="w-full rounded-md"
style={delayStyle(i * 70, { height: `${h}%` })}
/>
</div>
))}
</div>
<div className="mt-4 flex items-center justify-between">
{[0, 1, 2, 3, 4, 5].map((i) => (
<Block key={i} className="h-2.5 w-8 rounded-full" style={delayStyle(i * 50)} />
))}
</div>
</div>
<div className={`${CARD} ${chartPad}`}>
<Block className="h-4 w-32 rounded-full" style={delayStyle(0)} />
<div className="mt-6 flex justify-center">
<div className="relative h-40 w-40">
<Block className="h-40 w-40 rounded-full" style={delayStyle(120)} />
<div className="absolute left-1/2 top-1/2 h-20 w-20 -translate-x-1/2 -translate-y-1/2 rounded-full bg-white dark:bg-slate-900" />
</div>
</div>
<div className="mt-6 space-y-3">
{[0, 1, 2, 3].map((i) => (
<div key={i} className="flex items-center gap-3">
<Block className="h-3 w-3 rounded-sm" style={delayStyle(i * 90)} />
<Block className="h-3 flex-1 rounded-full" style={delayStyle(i * 90 + 30)} />
<Block className="h-3 w-10 rounded-full" style={delayStyle(i * 90 + 60)} />
</div>
))}
</div>
</div>
</div>
{/* Data table */}
<div className={`${CARD} overflow-hidden`}>
<div className={`flex items-center justify-between ${pad}`}>
<div className="space-y-2">
<Block className="h-4 w-36 rounded-full" style={delayStyle(0)} />
<Block className="h-3 w-24 rounded-full" style={delayStyle(50)} />
</div>
<Block className="h-9 w-24 rounded-lg" style={delayStyle(90)} />
</div>
<div className="hidden items-center gap-4 border-t border-slate-100 bg-slate-50/60 px-5 py-3 sm:flex dark:border-slate-800 dark:bg-slate-800/40">
<Block className="h-2.5 w-40 rounded-full" style={delayStyle(0)} />
<Block className="h-2.5 w-24 rounded-full" style={delayStyle(40)} />
<Block className="ml-auto h-2.5 w-16 rounded-full" style={delayStyle(80)} />
<Block className="h-2.5 w-16 rounded-full" style={delayStyle(120)} />
</div>
<ul className="divide-y divide-slate-100 dark:divide-slate-800">
{Array.from({ length: rows }).map((_, i) => (
<li
key={i}
className={`flex items-center gap-4 px-5 ${compact ? "py-3" : "py-4"}`}
>
<Block className="h-10 w-10 shrink-0 rounded-full" style={delayStyle(i * 90)} />
<div className="min-w-0 flex-1 space-y-2">
<Block className="h-3 w-32 max-w-full rounded-full" style={delayStyle(i * 90 + 30)} />
<Block className="h-3 w-24 max-w-full rounded-full" style={delayStyle(i * 90 + 55)} />
</div>
<Block className="hidden h-3 w-20 rounded-full sm:block" style={delayStyle(i * 90 + 80)} />
<Block className="hidden h-6 w-16 rounded-full md:block" style={delayStyle(i * 90 + 105)} />
<Block className="h-6 w-6 shrink-0 rounded-md" style={delayStyle(i * 90 + 130)} />
</li>
))}
</ul>
</div>
</div>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Card Skeleton
Originalcard skeleton placeholder with shimmer

List Skeleton
Originallist rows skeleton

Profile Skeleton
Originalprofile header skeleton

Article Skeleton
Originalarticle/text skeleton

Table Skeleton
Originaltable skeleton

Gallery Skeleton
Originalimage grid skeleton

Comment Skeleton
Originalcomment thread skeleton

Spotlight Hero
OriginalA centred hero with a soft radial spotlight, badge and dual call-to-action.

Split Hero
OriginalA two-column hero pairing a headline and CTAs with a product mock and social proof.

Gradient Spotlight Hero
OriginalA minimal centred hero with a soft gradient-mesh backdrop, announcement pill and dual call-to-action buttons.

App Preview Hero
OriginalA centred hero that pairs headline copy with a realistic product dashboard mock built entirely from markup, complete with browser chrome and a floating notification card.

Waitlist Capture Hero
OriginalA dark, focused hero with an inline email capture form and avatar social proof, ready for pre-launch waitlists.

