Web InnoventixFreeCode

Empty No Data

Original · free

empty data state with illustration

byWeb InnoventixReact + Tailwind
emptynodatastates
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/empty-no-data.json
empty-no-data.tsx
"use client";

import type { KeyboardEvent } from "react";
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";

type TabId = "npm" | "script" | "http";

type Snippet = {
  id: TabId;
  label: string;
  code: string;
};

type Step = {
  id: string;
  title: string;
  detail: string;
};

type Point = {
  day: string;
  value: number;
};

const SNIPPETS: Snippet[] = [
  {
    id: "npm",
    label: "npm",
    code: `npm i @trailhead/analytics

import { trailhead } from "@trailhead/analytics";

trailhead.init({ key: "th_live_9f2c4a" });
trailhead.track("signup_completed");`,
  },
  {
    id: "script",
    label: "Script tag",
    code: `<script
  src="https://cdn.trailhead.dev/v2/th.js"
  data-key="th_live_9f2c4a"
  defer
></script>`,
  },
  {
    id: "http",
    label: "HTTP API",
    code: `curl https://api.trailhead.dev/v2/events \\
  -H "Authorization: Bearer th_live_9f2c4a" \\
  -H "Content-Type: application/json" \\
  -d '{"event":"signup_completed","user":"u_1042"}'`,
  },
];

const STEPS: Step[] = [
  {
    id: "install",
    title: "Install the SDK",
    detail: "4.1 kB gzipped, no cookies, no consent banner required.",
  },
  {
    id: "event",
    title: "Send your first event",
    detail: "Page views are automatic. Call track() for everything else.",
  },
  {
    id: "verify",
    title: "Verify the live stream",
    detail: "Watch events land in the debugger before you ship to production.",
  },
  {
    id: "invite",
    title: "Invite your team",
    detail: "Dashboards and funnels stay shared from the very first event.",
  },
];

const DEMO: Point[] = [
  { day: "Mon", value: 1240 },
  { day: "Tue", value: 1890 },
  { day: "Wed", value: 1560 },
  { day: "Thu", value: 2310 },
  { day: "Fri", value: 2740 },
  { day: "Sat", value: 1180 },
  { day: "Sun", value: 3120 },
];

const GHOST: number[] = [0.34, 0.55, 0.42, 0.66, 0.78, 0.5, 0.86];

const AXIS_MAX = 3200;
const PLOT_TOP = 12;
const PLOT_BOTTOM = 138;
const PLOT_H = PLOT_BOTTOM - PLOT_TOP;
const BAR_W = 22;
const SLOT_W = 42;
const FIRST_X = 46;

const TICKS: number[] = [0, 1000, 2000, 3000];

function yFor(value: number): number {
  return PLOT_BOTTOM - (value / AXIS_MAX) * PLOT_H;
}

function barX(index: number): number {
  return FIRST_X + index * SLOT_W;
}

function tickLabel(value: number): string {
  return value === 0 ? "0" : `${value / 1000}k`;
}

function formatCount(value: number): string {
  return value.toLocaleString("en-US");
}

function CheckIcon({ className }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={3}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className={className}
    >
      <path d="M5 12.5l4.5 4.5L19 7" />
    </svg>
  );
}

function CopyIcon({ className }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.8}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className={className}
    >
      <rect x="9" y="9" width="11" height="11" rx="2.5" />
      <path d="M15 5.5A2.5 2.5 0 0012.5 3h-7A2.5 2.5 0 003 5.5v7A2.5 2.5 0 005.5 15" />
    </svg>
  );
}

function BookIcon({ className }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.8}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className={className}
    >
      <path d="M4 5.5A2.5 2.5 0 016.5 3H19v15H6.5A2.5 2.5 0 004 20.5z" />
      <path d="M4 20.5A2.5 2.5 0 016.5 18H19v3H6.5" />
    </svg>
  );
}

function EmptyTrayIcon({ className }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 64 64"
      fill="none"
      aria-hidden="true"
      className={className}
    >
      <ellipse
        cx="32"
        cy="16"
        rx="19"
        ry="7.5"
        stroke="currentColor"
        strokeWidth={2.5}
        strokeDasharray="5 6"
      />
      <path
        d="M13 16v14c0 4.1 8.5 7.5 19 7.5s19-3.4 19-7.5V16"
        stroke="currentColor"
        strokeWidth={2.5}
        strokeDasharray="5 6"
        strokeLinecap="round"
      />
      <path
        d="M13 30v14c0 4.1 8.5 7.5 19 7.5s19-3.4 19-7.5V30"
        stroke="currentColor"
        strokeWidth={2.5}
        strokeDasharray="5 6"
        strokeLinecap="round"
      />
    </svg>
  );
}

export default function EmptyNoData() {
  const reduce = useReducedMotion();
  const headingId = useId();
  const switchId = useId();
  const chartTitleId = useId();
  const statusId = useId();
  const panelId = useId();

  const [demo, setDemo] = useState<boolean>(false);
  const [tab, setTab] = useState<TabId>("npm");
  const [done, setDone] = useState<string[]>(["install"]);
  const [copied, setCopied] = useState<boolean>(false);

  const tabRefs = useRef<Array<HTMLButtonElement | null>>([]);
  const copyTimer = useRef<number | null>(null);

  useEffect(() => {
    return () => {
      if (copyTimer.current !== null) {
        window.clearTimeout(copyTimer.current);
      }
    };
  }, []);

  const active = useMemo<Snippet>(() => {
    const found = SNIPPETS.find((s) => s.id === tab);
    return found ?? SNIPPETS[0];
  }, [tab]);

  const completed = done.length;
  const progress = Math.round((completed / STEPS.length) * 100);
  const allDone = completed === STEPS.length;

  const totals = useMemo(() => {
    const views = DEMO.reduce((sum, p) => sum + p.value, 0);
    return { views, visitors: 3486, session: "2m 41s" };
  }, []);

  const toggleStep = useCallback((id: string) => {
    setDone((prev) =>
      prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id],
    );
  }, []);

  const copySnippet = useCallback(async () => {
    try {
      await navigator.clipboard.writeText(active.code);
      setCopied(true);
      if (copyTimer.current !== null) {
        window.clearTimeout(copyTimer.current);
      }
      copyTimer.current = window.setTimeout(() => setCopied(false), 2000);
    } catch {
      setCopied(false);
    }
  }, [active.code]);

  const onTabKeyDown = useCallback(
    (event: KeyboardEvent<HTMLButtonElement>, index: number) => {
      const keys = ["ArrowRight", "ArrowLeft", "Home", "End"];
      if (!keys.includes(event.key)) return;
      event.preventDefault();
      let next = index;
      if (event.key === "ArrowRight") next = (index + 1) % SNIPPETS.length;
      if (event.key === "ArrowLeft")
        next = (index - 1 + SNIPPETS.length) % SNIPPETS.length;
      if (event.key === "Home") next = 0;
      if (event.key === "End") next = SNIPPETS.length - 1;
      setTab(SNIPPETS[next].id);
      tabRefs.current[next]?.focus();
    },
    [],
  );

  const fade = reduce
    ? { initial: false as const, animate: { opacity: 1 } }
    : {
        initial: { opacity: 0, y: 8 },
        animate: { opacity: 1, y: 0 },
        exit: { opacity: 0, y: -6 },
        transition: { duration: 0.26, ease: [0.22, 1, 0.36, 1] as const },
      };

  return (
    <section
      aria-labelledby={headingId}
      className="relative w-full overflow-hidden bg-slate-50 px-4 py-20 text-slate-900 sm:px-6 sm:py-28 dark:bg-slate-950 dark:text-slate-100"
    >
      <style>{`
        @keyframes endat-ghost {
          0%, 100% { opacity: .5; }
          50% { opacity: .85; }
        }
        @keyframes endat-drift {
          0%, 100% { transform: translateY(0); }
          50% { transform: translateY(-6px); }
        }
        @keyframes endat-sweep {
          0% { transform: translateX(-120%); }
          100% { transform: translateX(220%); }
        }
        .endat-ghost { animation: endat-ghost 3.2s ease-in-out infinite; }
        .endat-drift { animation: endat-drift 6s ease-in-out infinite; }
        .endat-sweep { animation: endat-sweep 3.6s ease-in-out infinite; }
        @media (prefers-reduced-motion: reduce) {
          .endat-ghost, .endat-drift, .endat-sweep { animation: none !important; }
        }
      `}</style>

      <div
        aria-hidden="true"
        className="pointer-events-none absolute left-1/2 top-0 h-64 w-[42rem] -translate-x-1/2 rounded-full bg-indigo-400/10 blur-3xl dark:bg-indigo-500/10"
      />

      <div className="relative mx-auto max-w-5xl">
        <div className="flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-end">
          <div>
            <p className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white px-3 py-1 text-xs font-medium uppercase tracking-wider text-slate-500 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-400">
              <span
                aria-hidden="true"
                className="h-1.5 w-1.5 rounded-full bg-amber-500"
              />
              Awaiting first event
            </p>
            <h2
              id={headingId}
              className="mt-4 text-2xl font-bold tracking-tight text-slate-900 sm:text-3xl dark:text-white"
            >
              Trailhead Analytics
            </h2>
            <p className="mt-2 max-w-lg text-sm text-slate-600 sm:text-base dark:text-slate-400">
              This workspace has no traffic data yet. Once the SDK sends its
              first event, charts fill in automatically — usually within 60
              seconds.
            </p>
          </div>

          <div className="flex items-center gap-3">
            <label
              htmlFor={switchId}
              className="cursor-pointer select-none text-sm font-medium text-slate-700 dark:text-slate-300"
            >
              Sample data
            </label>
            <button
              id={switchId}
              type="button"
              role="switch"
              aria-checked={demo}
              aria-describedby={statusId}
              onClick={() => setDemo((v) => !v)}
              className={`relative inline-flex h-6 w-11 shrink-0 items-center rounded-full border transition-colors 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 ${
                demo
                  ? "border-indigo-600 bg-indigo-600"
                  : "border-slate-300 bg-slate-200 dark:border-slate-700 dark:bg-slate-800"
              }`}
            >
              <span
                aria-hidden="true"
                className={`inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform ${
                  demo ? "translate-x-6" : "translate-x-1"
                }`}
              />
            </button>
          </div>
        </div>

        <p id={statusId} role="status" aria-live="polite" className="sr-only">
          {demo
            ? `Showing sample data: ${formatCount(totals.views)} page views across 7 days.`
            : "No data collected yet. Chart is empty."}
        </p>

        <div className="mt-8 grid gap-6 lg:grid-cols-5">
          {/* Chart / illustration */}
          <div className="lg:col-span-3">
            <div className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm sm:p-6 dark:border-slate-800 dark:bg-slate-900">
              <div className="flex items-center justify-between gap-3">
                <div>
                  <h3
                    id={chartTitleId}
                    className="text-sm font-semibold text-slate-900 dark:text-white"
                  >
                    Page views
                  </h3>
                  <p className="text-xs text-slate-500 dark:text-slate-400">
                    Last 7 days
                  </p>
                </div>
                {demo ? (
                  <span className="rounded-md bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-800 dark:bg-amber-500/15 dark:text-amber-300">
                    Sample
                  </span>
                ) : (
                  <span className="rounded-md bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-500 dark:bg-slate-800 dark:text-slate-400">
                    Empty
                  </span>
                )}
              </div>

              <div className="relative mt-4">
                <svg
                  viewBox="0 0 340 170"
                  role="img"
                  aria-labelledby={chartTitleId}
                  className="h-auto w-full"
                >
                  {/* grid + axis */}
                  <g
                    className="text-slate-200 dark:text-slate-800"
                    stroke="currentColor"
                    strokeWidth={1}
                  >
                    {TICKS.map((t) => (
                      <line
                        key={`grid-${t}`}
                        x1={36}
                        x2={330}
                        y1={yFor(t)}
                        y2={yFor(t)}
                        strokeDasharray={t === 0 ? undefined : "3 5"}
                      />
                    ))}
                  </g>
                  <g
                    className="fill-slate-400 dark:fill-slate-500"
                    fontSize={9}
                    textAnchor="end"
                  >
                    {TICKS.map((t) => (
                      <text key={`tick-${t}`} x={29} y={yFor(t) + 3}>
                        {tickLabel(t)}
                      </text>
                    ))}
                  </g>
                  <g
                    className="fill-slate-400 dark:fill-slate-500"
                    fontSize={9}
                    textAnchor="middle"
                  >
                    {DEMO.map((p, i) => (
                      <text key={p.day} x={barX(i) + BAR_W / 2} y={154}>
                        {p.day}
                      </text>
                    ))}
                  </g>

                  {/* bars */}
                  {demo
                    ? DEMO.map((p, i) => {
                        const h = (p.value / AXIS_MAX) * PLOT_H;
                        return (
                          <motion.rect
                            key={`bar-${p.day}`}
                            x={barX(i)}
                            y={PLOT_BOTTOM - h}
                            width={BAR_W}
                            height={h}
                            rx={4}
                            className="fill-indigo-500 dark:fill-indigo-400"
                            style={{
                              transformBox: "fill-box",
                              transformOrigin: "bottom",
                            }}
                            initial={reduce ? false : { scaleY: 0 }}
                            animate={{ scaleY: 1 }}
                            transition={
                              reduce
                                ? undefined
                                : {
                                    duration: 0.45,
                                    delay: 0.05 + i * 0.06,
                                    ease: [0.22, 1, 0.36, 1],
                                  }
                            }
                          />
                        );
                      })
                    : GHOST.map((f, i) => (
                        <rect
                          key={`ghost-${DEMO[i].day}`}
                          x={barX(i)}
                          y={PLOT_BOTTOM - f * PLOT_H}
                          width={BAR_W}
                          height={f * PLOT_H}
                          rx={4}
                          className="endat-ghost fill-slate-100 stroke-slate-300 dark:fill-slate-800/60 dark:stroke-slate-700"
                          strokeWidth={1.5}
                          strokeDasharray="4 5"
                          style={{ animationDelay: `${i * 0.18}s` }}
                        />
                      ))}

                  {/* baseline */}
                  <line
                    x1={36}
                    x2={330}
                    y1={PLOT_BOTTOM}
                    y2={PLOT_BOTTOM}
                    className="stroke-slate-300 dark:stroke-slate-700"
                    strokeWidth={1.5}
                  />
                </svg>

                <AnimatePresence initial={false}>
                  {!demo ? (
                    <motion.div
                      key="overlay"
                      {...fade}
                      className="absolute inset-0 flex flex-col items-center justify-center rounded-xl bg-white/70 backdrop-blur-[2px] dark:bg-slate-900/70"
                    >
                      <EmptyTrayIcon className="endat-drift h-12 w-12 text-slate-300 dark:text-slate-600" />
                      <p className="mt-3 text-sm font-semibold text-slate-800 dark:text-slate-100">
                        No data yet
                      </p>
                      <p className="mt-1 max-w-[16rem] text-center text-xs text-slate-500 dark:text-slate-400">
                        Nothing has been tracked for this workspace. Finish
                        setup, or flip on sample data to see the shape of it.
                      </p>
                      <button
                        type="button"
                        onClick={() => setDemo(true)}
                        className="mt-4 rounded-lg border border-slate-300 bg-white px-3 py-1.5 text-xs font-semibold text-slate-700 shadow-sm transition hover:border-indigo-400 hover:text-indigo-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:border-indigo-500 dark:hover:text-indigo-400 dark:focus-visible:ring-offset-slate-900"
                      >
                        Show sample data
                      </button>
                    </motion.div>
                  ) : null}
                </AnimatePresence>
              </div>

              <dl className="mt-5 grid grid-cols-3 gap-3 border-t border-slate-200 pt-5 dark:border-slate-800">
                <Stat
                  label="Page views"
                  value={demo ? formatCount(totals.views) : "—"}
                  muted={!demo}
                />
                <Stat
                  label="Visitors"
                  value={demo ? formatCount(totals.visitors) : "—"}
                  muted={!demo}
                />
                <Stat
                  label="Median session"
                  value={demo ? totals.session : "—"}
                  muted={!demo}
                />
              </dl>
            </div>
          </div>

          {/* Setup checklist */}
          <div className="lg:col-span-2">
            <div className="flex h-full flex-col rounded-2xl border border-slate-200 bg-white p-5 shadow-sm sm:p-6 dark:border-slate-800 dark:bg-slate-900">
              <div className="flex items-baseline justify-between gap-2">
                <h3 className="text-sm font-semibold text-slate-900 dark:text-white">
                  Finish setup
                </h3>
                <p className="text-xs font-medium tabular-nums text-slate-500 dark:text-slate-400">
                  {completed} of {STEPS.length}
                </p>
              </div>

              <div
                role="progressbar"
                aria-valuemin={0}
                aria-valuemax={100}
                aria-valuenow={progress}
                aria-label="Setup progress"
                className="relative mt-3 h-1.5 w-full overflow-hidden rounded-full bg-slate-200 dark:bg-slate-800"
              >
                <motion.div
                  className={`h-full rounded-full ${allDone ? "bg-emerald-500" : "bg-indigo-500"}`}
                  initial={false}
                  animate={{ width: `${progress}%` }}
                  transition={
                    reduce ? { duration: 0 } : { duration: 0.4, ease: "easeOut" }
                  }
                />
                {!allDone ? (
                  <span
                    aria-hidden="true"
                    className="endat-sweep pointer-events-none absolute inset-y-0 left-0 w-1/3 bg-gradient-to-r from-transparent via-white/70 to-transparent dark:via-white/20"
                  />
                ) : null}
              </div>

              <ul className="mt-5 space-y-2">
                {STEPS.map((step) => {
                  const checked = done.includes(step.id);
                  return (
                    <li key={step.id}>
                      <label className="group flex cursor-pointer items-start gap-3 rounded-xl border border-transparent p-2.5 transition hover:border-slate-200 hover:bg-slate-50 dark:hover:border-slate-800 dark:hover:bg-slate-800/50">
                        <input
                          type="checkbox"
                          checked={checked}
                          onChange={() => toggleStep(step.id)}
                          className="peer sr-only"
                        />
                        <span
                          aria-hidden="true"
                          className={`mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-md border transition peer-focus-visible:ring-2 peer-focus-visible:ring-indigo-500 peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-white dark:peer-focus-visible:ring-offset-slate-900 ${
                            checked
                              ? "border-emerald-500 bg-emerald-500 text-white"
                              : "border-slate-300 bg-white text-transparent dark:border-slate-600 dark:bg-slate-900"
                          }`}
                        >
                          <CheckIcon className="h-3 w-3" />
                        </span>
                        <span className="min-w-0">
                          <span
                            className={`block text-sm font-medium transition ${
                              checked
                                ? "text-slate-400 line-through dark:text-slate-500"
                                : "text-slate-900 dark:text-slate-100"
                            }`}
                          >
                            {step.title}
                          </span>
                          <span className="mt-0.5 block text-xs text-slate-500 dark:text-slate-400">
                            {step.detail}
                          </span>
                        </span>
                      </label>
                    </li>
                  );
                })}
              </ul>

              <AnimatePresence initial={false}>
                {allDone ? (
                  <motion.p
                    key="ready"
                    {...fade}
                    className="mt-4 rounded-xl border border-emerald-200 bg-emerald-50 px-3 py-2.5 text-xs font-medium text-emerald-800 dark:border-emerald-500/30 dark:bg-emerald-500/10 dark:text-emerald-300"
                  >
                    Setup complete. Your first events should appear here within a
                    minute of real traffic.
                  </motion.p>
                ) : null}
              </AnimatePresence>

              <div className="mt-auto flex flex-wrap gap-2 pt-5">
                <a
                  href="https://trailhead.dev/docs/quickstart"
                  target="_blank"
                  rel="noopener"
                  className="inline-flex items-center gap-2 rounded-lg border border-slate-300 bg-white px-3 py-2 text-xs font-semibold text-slate-700 shadow-sm transition hover:border-indigo-400 hover:text-indigo-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:border-indigo-500 dark:hover:text-indigo-400 dark:focus-visible:ring-offset-slate-900"
                >
                  <BookIcon className="h-3.5 w-3.5" />
                  Read the quickstart
                </a>
              </div>
            </div>
          </div>
        </div>

        {/* Install snippet */}
        <div className="mt-6 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm sm:p-6 dark:border-slate-800 dark:bg-slate-900">
          <div className="flex flex-wrap items-center justify-between gap-3">
            <div>
              <h3 className="text-sm font-semibold text-slate-900 dark:text-white">
                Send your first event
              </h3>
              <p className="text-xs text-slate-500 dark:text-slate-400">
                Paste this into your app. The key below is scoped to this
                workspace.
              </p>
            </div>
            <div
              role="tablist"
              aria-label="Installation method"
              className="inline-flex rounded-lg border border-slate-200 bg-slate-100 p-1 dark:border-slate-800 dark:bg-slate-800/60"
            >
              {SNIPPETS.map((s, i) => {
                const selected = s.id === tab;
                return (
                  <button
                    key={s.id}
                    ref={(el) => {
                      tabRefs.current[i] = el;
                    }}
                    id={`${panelId}-tab-${s.id}`}
                    type="button"
                    role="tab"
                    aria-selected={selected}
                    aria-controls={panelId}
                    tabIndex={selected ? 0 : -1}
                    onClick={() => setTab(s.id)}
                    onKeyDown={(e) => onTabKeyDown(e, i)}
                    className={`rounded-md px-3 py-1.5 text-xs font-semibold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-100 dark:focus-visible:ring-offset-slate-800 ${
                      selected
                        ? "bg-white text-slate-900 shadow-sm dark:bg-slate-950 dark:text-white"
                        : "text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-slate-200"
                    }`}
                  >
                    {s.label}
                  </button>
                );
              })}
            </div>
          </div>

          <div
            id={panelId}
            role="tabpanel"
            tabIndex={0}
            aria-labelledby={`${panelId}-tab-${active.id}`}
            className="relative mt-4 rounded-xl border border-slate-200 bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:border-slate-800 dark:bg-slate-950"
          >
            <button
              type="button"
              onClick={copySnippet}
              className="absolute right-2.5 top-2.5 z-10 inline-flex items-center gap-1.5 rounded-lg border border-slate-200 bg-white px-2.5 py-1.5 text-xs font-medium text-slate-600 shadow-sm transition hover:text-indigo-600 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:border-slate-700 dark:bg-slate-900 dark:text-slate-300 dark:hover:text-indigo-400 dark:focus-visible:ring-offset-slate-950"
            >
              {copied ? (
                <CheckIcon className="h-3.5 w-3.5 text-emerald-500" />
              ) : (
                <CopyIcon className="h-3.5 w-3.5" />
              )}
              {copied ? "Copied" : "Copy"}
            </button>
            <div className="overflow-x-auto p-4 pr-24">
              <pre className="text-xs leading-relaxed text-slate-700 dark:text-slate-300">
                <code>{active.code}</code>
              </pre>
            </div>
          </div>

          <p role="status" aria-live="polite" className="sr-only">
            {copied ? "Snippet copied to clipboard." : ""}
          </p>
        </div>
      </div>
    </section>
  );
}

function Stat({
  label,
  value,
  muted,
}: {
  label: string;
  value: string;
  muted: boolean;
}) {
  return (
    <div>
      <dt className="text-xs text-slate-500 dark:text-slate-400">{label}</dt>
      <dd
        className={`mt-0.5 text-lg font-semibold tabular-nums ${
          muted
            ? "text-slate-300 dark:text-slate-600"
            : "text-slate-900 dark:text-white"
        }`}
      >
        {value}
      </dd>
    </div>
  );
}

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 →