Web InnoventixFreeCode

Dashboard Skeleton

Original · free

dashboard widgets skeleton

byWeb InnoventixReact + Tailwind
skeldashboardskeletons
Open

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
skel-dashboard.tsx
"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

motion

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 quote

Similar components

Browse all →