Web InnoventixFreeCode

Drawer Left Sidebar

Original · free

slide-in left drawer with overlay

byWeb InnoventixReact + Tailwind
sidedrawerleftsidebars
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/side-drawer-left.json
side-drawer-left.tsx
"use client";

import {
  type KeyboardEvent as ReactKeyboardEvent,
  type ReactElement,
  type ReactNode,
  useCallback,
  useEffect,
  useId,
  useRef,
  useState,
} from "react";
import {
  AnimatePresence,
  motion,
  type Transition,
  useReducedMotion,
} from "motion/react";

type IconProps = { className?: string };

type NavItem = {
  id: string;
  label: string;
  hint: string;
  icon: (props: IconProps) => ReactElement;
  badge?: string;
  tone?: "amber" | "emerald";
};

type NavGroup = {
  id: string;
  title: string;
  items: NavItem[];
};

function MenuIcon({ className }: IconProps) {
  return (
    <svg className={className} viewBox="0 0 24 24" fill="none" aria-hidden="true">
      <path d="M4 6h16M4 12h16M4 18h16" stroke="currentColor" strokeWidth={1.9} strokeLinecap="round" />
    </svg>
  );
}

function CloseIcon({ className }: IconProps) {
  return (
    <svg className={className} viewBox="0 0 24 24" fill="none" aria-hidden="true">
      <path d="M6 6l12 12M18 6L6 18" stroke="currentColor" strokeWidth={1.9} strokeLinecap="round" />
    </svg>
  );
}

function SearchIcon({ className }: IconProps) {
  return (
    <svg className={className} viewBox="0 0 24 24" fill="none" aria-hidden="true">
      <circle cx="11" cy="11" r="6.5" stroke="currentColor" strokeWidth={1.8} />
      <path d="M20 20l-3.6-3.6" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" />
    </svg>
  );
}

function OverviewIcon({ className }: IconProps) {
  return (
    <svg className={className} viewBox="0 0 24 24" fill="none" aria-hidden="true">
      <rect x="3.5" y="3.5" width="7" height="7" rx="1.6" stroke="currentColor" strokeWidth={1.7} />
      <rect x="13.5" y="3.5" width="7" height="4.5" rx="1.4" stroke="currentColor" strokeWidth={1.7} />
      <rect x="13.5" y="11" width="7" height="9.5" rx="1.6" stroke="currentColor" strokeWidth={1.7} />
      <rect x="3.5" y="13.5" width="7" height="7" rx="1.6" stroke="currentColor" strokeWidth={1.7} />
    </svg>
  );
}

function DeployIcon({ className }: IconProps) {
  return (
    <svg className={className} viewBox="0 0 24 24" fill="none" aria-hidden="true">
      <path d="M14.5 3.5c3 .3 5.7 3 6 6-2 5-6 8.5-8.5 9.5L8 15.5C9 13 12.5 9 14.5 3.5Z" stroke="currentColor" strokeWidth={1.7} strokeLinejoin="round" />
      <circle cx="14.5" cy="9.5" r="1.7" stroke="currentColor" strokeWidth={1.7} />
      <path d="M8 15.5 5 18M6.5 20l-1.8-1.8M9.5 17l1.8 1.8" stroke="currentColor" strokeWidth={1.7} strokeLinecap="round" />
    </svg>
  );
}

function PulseIcon({ className }: IconProps) {
  return (
    <svg className={className} viewBox="0 0 24 24" fill="none" aria-hidden="true">
      <path d="M3 12h4l2.5-6 4 13 2.5-7H21" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  );
}

function AlertIcon({ className }: IconProps) {
  return (
    <svg className={className} viewBox="0 0 24 24" fill="none" aria-hidden="true">
      <path d="M12 3.8 21 19.5H3L12 3.8Z" stroke="currentColor" strokeWidth={1.7} strokeLinejoin="round" />
      <path d="M12 10v4" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" />
      <circle cx="12" cy="17" r="0.6" fill="currentColor" stroke="currentColor" strokeWidth={0.8} />
    </svg>
  );
}

function DatabaseIcon({ className }: IconProps) {
  return (
    <svg className={className} viewBox="0 0 24 24" fill="none" aria-hidden="true">
      <ellipse cx="12" cy="6" rx="7" ry="2.8" stroke="currentColor" strokeWidth={1.7} />
      <path d="M5 6v12c0 1.5 3.1 2.8 7 2.8s7-1.3 7-2.8V6" stroke="currentColor" strokeWidth={1.7} />
      <path d="M5 12c0 1.5 3.1 2.8 7 2.8s7-1.3 7-2.8" stroke="currentColor" strokeWidth={1.7} />
    </svg>
  );
}

function NetworkIcon({ className }: IconProps) {
  return (
    <svg className={className} viewBox="0 0 24 24" fill="none" aria-hidden="true">
      <circle cx="6" cy="6" r="2.4" stroke="currentColor" strokeWidth={1.7} />
      <circle cx="18" cy="7" r="2.4" stroke="currentColor" strokeWidth={1.7} />
      <circle cx="12" cy="18" r="2.4" stroke="currentColor" strokeWidth={1.7} />
      <path d="M7.7 7.6 10.6 16M16.3 8.8 13.4 16M8.2 6.4h7.4" stroke="currentColor" strokeWidth={1.7} strokeLinecap="round" />
    </svg>
  );
}

function MembersIcon({ className }: IconProps) {
  return (
    <svg className={className} viewBox="0 0 24 24" fill="none" aria-hidden="true">
      <circle cx="9" cy="8" r="3" stroke="currentColor" strokeWidth={1.7} />
      <path d="M3.5 19c.5-3.1 2.9-5 5.5-5s5 1.9 5.5 5" stroke="currentColor" strokeWidth={1.7} strokeLinecap="round" />
      <path d="M16 6.2A2.8 2.8 0 0 1 17.6 11M17 14.4c2 .5 3.5 2.2 3.9 4.6" stroke="currentColor" strokeWidth={1.7} strokeLinecap="round" />
    </svg>
  );
}

function BillingIcon({ className }: IconProps) {
  return (
    <svg className={className} viewBox="0 0 24 24" fill="none" aria-hidden="true">
      <rect x="3" y="5.5" width="18" height="13" rx="2.2" stroke="currentColor" strokeWidth={1.7} />
      <path d="M3 9.5h18M6.5 14.5h4" stroke="currentColor" strokeWidth={1.7} strokeLinecap="round" />
    </svg>
  );
}

function SettingsIcon({ className }: IconProps) {
  return (
    <svg className={className} viewBox="0 0 24 24" fill="none" aria-hidden="true">
      <circle cx="12" cy="12" r="2.8" stroke="currentColor" strokeWidth={1.7} />
      <path d="M12 3v2.4M12 18.6V21M4.2 7.5l2 1.2M17.8 15.3l2 1.2M4.2 16.5l2-1.2M17.8 8.7l2-1.2" stroke="currentColor" strokeWidth={1.7} strokeLinecap="round" />
    </svg>
  );
}

function LogoutIcon({ className }: IconProps) {
  return (
    <svg className={className} viewBox="0 0 24 24" fill="none" aria-hidden="true">
      <path d="M14 5.5H6.5A1.5 1.5 0 0 0 5 7v10a1.5 1.5 0 0 0 1.5 1.5H14" stroke="currentColor" strokeWidth={1.7} strokeLinecap="round" />
      <path d="M17 8.5 20.5 12 17 15.5M20 12h-9" stroke="currentColor" strokeWidth={1.7} strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  );
}

function BrandMark({ className }: IconProps) {
  return (
    <svg className={className} viewBox="0 0 32 32" fill="none" aria-hidden="true">
      <rect width="32" height="32" rx="9" fill="url(#sdl-brand)" />
      <path d="M8 22V10l8 7 8-7v12" stroke="white" strokeWidth={2.4} strokeLinecap="round" strokeLinejoin="round" />
      <defs>
        <linearGradient id="sdl-brand" x1="0" y1="0" x2="32" y2="32" gradientUnits="userSpaceOnUse">
          <stop stopColor="#818cf8" />
          <stop offset="1" stopColor="#8b5cf6" />
        </linearGradient>
      </defs>
    </svg>
  );
}

const NAV_GROUPS: NavGroup[] = [
  {
    id: "workspace",
    title: "Workspace",
    items: [
      { id: "overview", label: "Overview", hint: "Fleet health at a glance", icon: OverviewIcon },
      { id: "deployments", label: "Deployments", hint: "34 shipped this week", icon: DeployIcon },
      { id: "observability", label: "Observability", hint: "Traces, logs & metrics", icon: PulseIcon },
      { id: "incidents", label: "Incidents", hint: "2 active, 1 acknowledged", icon: AlertIcon, badge: "2", tone: "amber" },
    ],
  },
  {
    id: "resources",
    title: "Resources",
    items: [
      { id: "databases", label: "Databases", hint: "6 clusters · Postgres", icon: DatabaseIcon },
      { id: "networking", label: "Networking", hint: "Routes, edges & DNS", icon: NetworkIcon },
    ],
  },
  {
    id: "org",
    title: "Team & billing",
    items: [
      { id: "members", label: "Members", hint: "12 seats in use", icon: MembersIcon },
      { id: "billing", label: "Usage & billing", hint: "Renews Aug 1", icon: BillingIcon, badge: "Pro", tone: "emerald" },
      { id: "settings", label: "Settings", hint: "Workspace preferences", icon: SettingsIcon },
    ],
  },
];

const FOCUSABLE =
  'a[href],button:not([disabled]),input:not([disabled]),textarea,select,[tabindex]:not([tabindex="-1"])';

export default function SideDrawerLeft(): ReactElement {
  const reduce = useReducedMotion();
  const [open, setOpen] = useState(false);
  const [active, setActive] = useState("overview");
  const [query, setQuery] = useState("");

  const triggerRef = useRef<HTMLButtonElement>(null);
  const panelRef = useRef<HTMLDivElement>(null);
  const titleId = useId();
  const descId = useId();

  const close = useCallback(() => setOpen(false), []);

  // Body scroll lock while the drawer is open.
  useEffect(() => {
    if (!open) return;
    const prev = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    return () => {
      document.body.style.overflow = prev;
    };
  }, [open]);

  // Focus management: move focus into the panel on open, restore on close.
  useEffect(() => {
    if (!open) return;
    const previouslyFocused = document.activeElement as HTMLElement | null;
    const raf = window.requestAnimationFrame(() => {
      const node = panelRef.current;
      const first = node?.querySelector<HTMLElement>(FOCUSABLE);
      first?.focus();
    });
    return () => {
      window.cancelAnimationFrame(raf);
      const target = triggerRef.current ?? previouslyFocused;
      target?.focus();
    };
  }, [open]);

  const onPanelKeyDown = useCallback(
    (event: ReactKeyboardEvent<HTMLDivElement>) => {
      if (event.key === "Escape") {
        event.stopPropagation();
        close();
        return;
      }
      if (event.key !== "Tab") return;
      const node = panelRef.current;
      if (!node) return;
      const items = Array.from(node.querySelectorAll<HTMLElement>(FOCUSABLE)).filter(
        (el) => el.offsetParent !== null || el === document.activeElement,
      );
      if (items.length === 0) return;
      const firstEl = items[0];
      const lastEl = items[items.length - 1];
      const activeEl = document.activeElement as HTMLElement | null;
      if (event.shiftKey && activeEl === firstEl) {
        event.preventDefault();
        lastEl.focus();
      } else if (!event.shiftKey && activeEl === lastEl) {
        event.preventDefault();
        firstEl.focus();
      }
    },
    [close],
  );

  const filtered: NavGroup[] = NAV_GROUPS.map((group) => ({
    ...group,
    items: group.items.filter((item) =>
      item.label.toLowerCase().includes(query.trim().toLowerCase()),
    ),
  })).filter((group) => group.items.length > 0);

  const overlayTransition: Transition = reduce
    ? { duration: 0 }
    : { duration: 0.28, ease: [0.4, 0, 0.2, 1] };
  const panelTransition: Transition = reduce
    ? { duration: 0 }
    : { type: "spring", stiffness: 330, damping: 34, mass: 0.9 };

  return (
    <section className="relative w-full overflow-hidden bg-slate-100 px-4 py-16 text-slate-900 sm:px-8 md:py-24 dark:bg-slate-950 dark:text-slate-100">
      <style>{`
        @keyframes sdl-badge-pulse {
          0%, 100% { box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.45); }
          50% { box-shadow: 0 0 0 5px rgba(245, 158, 11, 0); }
        }
        @keyframes sdl-sheen {
          0% { transform: translateX(-120%); }
          60%, 100% { transform: translateX(220%); }
        }
        .sdl-badge-live { animation: sdl-badge-pulse 2.4s ease-out infinite; }
        .sdl-sheen { animation: sdl-sheen 4.5s ease-in-out infinite; }
        @media (prefers-reduced-motion: reduce) {
          .sdl-badge-live, .sdl-sheen { animation: none !important; }
        }
      `}</style>

      {/* Ambient backdrop texture */}
      <div
        aria-hidden="true"
        className="pointer-events-none absolute inset-0 opacity-70 [background:radial-gradient(60rem_40rem_at_-10%_-10%,rgba(129,140,248,0.18),transparent),radial-gradient(50rem_35rem_at_110%_10%,rgba(139,92,246,0.14),transparent)]"
      />

      <div className="relative mx-auto max-w-5xl">
        {/* Demo application chrome */}
        <div className="overflow-hidden rounded-3xl border border-slate-200 bg-white/80 shadow-xl shadow-slate-900/5 backdrop-blur dark:border-slate-800 dark:bg-slate-900/70 dark:shadow-black/30">
          <div className="flex items-center gap-3 border-b border-slate-200 px-4 py-3 sm:px-6 dark:border-slate-800">
            <button
              ref={triggerRef}
              type="button"
              onClick={() => setOpen(true)}
              aria-haspopup="dialog"
              aria-expanded={open}
              aria-controls={titleId}
              className="inline-flex items-center gap-2 rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm font-medium text-slate-700 transition hover:border-indigo-300 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-800 dark:text-slate-200 dark:hover:border-indigo-500 dark:hover:text-indigo-300 dark:focus-visible:ring-offset-slate-900"
            >
              <MenuIcon className="h-5 w-5" />
              <span>Menu</span>
            </button>

            <div className="flex items-center gap-2">
              <span className="hidden h-1.5 w-1.5 rounded-full bg-emerald-500 sm:inline-block" />
              <span className="text-sm font-semibold tracking-tight text-slate-800 dark:text-slate-100">
                Meridian
              </span>
              <span className="hidden text-xs text-slate-400 sm:inline dark:text-slate-500">
                / us-east cluster
              </span>
            </div>

            <div className="ml-auto flex items-center gap-2">
              <span className="rounded-full bg-emerald-50 px-2.5 py-1 text-[11px] font-semibold text-emerald-700 ring-1 ring-emerald-200 dark:bg-emerald-500/10 dark:text-emerald-300 dark:ring-emerald-500/25">
                All systems operational
              </span>
            </div>
          </div>

          <div className="grid gap-4 p-6 sm:grid-cols-3 sm:p-8">
            <StatCard label="p95 latency" value="128ms" delta="-9%" good />
            <StatCard label="Error rate" value="0.04%" delta="-0.01" good />
            <StatCard label="Active nodes" value="42" delta="+3" />
            <p className="text-sm leading-relaxed text-slate-500 sm:col-span-3 dark:text-slate-400">
              Open the navigation drawer to move between workspace sections. The drawer traps focus,
              closes on <kbd className="rounded bg-slate-100 px-1.5 py-0.5 text-[11px] font-semibold text-slate-600 ring-1 ring-slate-200 dark:bg-slate-800 dark:text-slate-300 dark:ring-slate-700">Esc</kbd>{" "}
              or overlay click, and returns focus to the button that opened it.
            </p>
          </div>
        </div>
      </div>

      {/* Drawer + overlay */}
      <AnimatePresence>
        {open && (
          <div className="fixed inset-0 z-50">
            <motion.button
              type="button"
              aria-label="Close navigation"
              tabIndex={-1}
              onClick={close}
              initial={{ opacity: 0 }}
              animate={{ opacity: 1 }}
              exit={{ opacity: 0 }}
              transition={overlayTransition}
              className="absolute inset-0 h-full w-full cursor-default bg-slate-950/55 backdrop-blur-sm"
            />

            <motion.div
              ref={panelRef}
              role="dialog"
              aria-modal="true"
              aria-labelledby={titleId}
              aria-describedby={descId}
              id={titleId}
              onKeyDown={onPanelKeyDown}
              initial={{ x: "-100%" }}
              animate={{ x: 0 }}
              exit={{ x: "-100%" }}
              transition={panelTransition}
              className="absolute inset-y-0 left-0 flex w-[86%] max-w-sm flex-col border-r border-slate-200 bg-white shadow-2xl shadow-black/25 dark:border-slate-800 dark:bg-slate-900"
            >
              {/* Header */}
              <div className="relative overflow-hidden border-b border-slate-200 px-5 pb-5 pt-6 dark:border-slate-800">
                <div
                  aria-hidden="true"
                  className="pointer-events-none absolute inset-0 [background:linear-gradient(120deg,rgba(129,140,248,0.12),transparent_45%)]"
                />
                <div className="pointer-events-none absolute inset-0 overflow-hidden">
                  <div className="sdl-sheen absolute -inset-y-4 left-0 w-1/3 skew-x-12 bg-gradient-to-r from-transparent via-white/50 to-transparent dark:via-white/10" />
                </div>
                <div className="relative flex items-center gap-3">
                  <BrandMark className="h-9 w-9 shrink-0" />
                  <div className="min-w-0">
                    <h2
                      id={descId}
                      className="truncate text-base font-semibold tracking-tight text-slate-900 dark:text-white"
                    >
                      Meridian Console
                    </h2>
                    <p className="truncate text-xs text-slate-500 dark:text-slate-400">
                      Northwind Robotics · Pro
                    </p>
                  </div>
                  <button
                    type="button"
                    onClick={close}
                    className="ml-auto grid h-9 w-9 shrink-0 place-items-center rounded-xl text-slate-500 transition hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-white"
                    aria-label="Close navigation"
                  >
                    <CloseIcon className="h-5 w-5" />
                  </button>
                </div>

                {/* Search */}
                <div className="relative mt-4">
                  <SearchIcon className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400 dark:text-slate-500" />
                  <input
                    type="text"
                    value={query}
                    onChange={(e) => setQuery(e.target.value)}
                    placeholder="Jump to a section"
                    aria-label="Filter navigation sections"
                    className="w-full rounded-xl border border-slate-200 bg-slate-50 py-2 pl-9 pr-3 text-sm text-slate-800 placeholder:text-slate-400 focus-visible:border-indigo-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500/40 dark:border-slate-700 dark:bg-slate-800/70 dark:text-slate-100 dark:placeholder:text-slate-500"
                  />
                </div>
              </div>

              {/* Nav */}
              <nav
                aria-label="Primary"
                className="flex-1 overflow-y-auto px-3 py-4 [scrollbar-width:thin]"
              >
                {filtered.length === 0 && (
                  <p className="px-3 py-6 text-center text-sm text-slate-400 dark:text-slate-500">
                    No sections match &ldquo;{query}&rdquo;.
                  </p>
                )}
                {filtered.map((group) => (
                  <div key={group.id} className="mb-5 last:mb-1">
                    <p className="px-3 pb-1.5 text-[11px] font-semibold uppercase tracking-wider text-slate-400 dark:text-slate-500">
                      {group.title}
                    </p>
                    <ul className="space-y-0.5">
                      {group.items.map((item) => {
                        const isActive = active === item.id;
                        return (
                          <li key={item.id}>
                            <button
                              type="button"
                              onClick={() => setActive(item.id)}
                              aria-current={isActive ? "page" : undefined}
                              className={[
                                "group relative flex w-full items-center gap-3 rounded-xl px-3 py-2.5 text-left transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500",
                                isActive
                                  ? "bg-indigo-50 text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-200"
                                  : "text-slate-700 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800/70",
                              ].join(" ")}
                            >
                              <span
                                aria-hidden="true"
                                className={[
                                  "absolute left-0 top-1/2 h-6 w-1 -translate-y-1/2 rounded-r-full bg-indigo-500 transition-opacity",
                                  isActive ? "opacity-100" : "opacity-0",
                                ].join(" ")}
                              />
                              <item.icon
                                className={[
                                  "h-5 w-5 shrink-0 transition-colors",
                                  isActive
                                    ? "text-indigo-600 dark:text-indigo-300"
                                    : "text-slate-400 group-hover:text-slate-600 dark:text-slate-500 dark:group-hover:text-slate-300",
                                ].join(" ")}
                              />
                              <span className="min-w-0 flex-1">
                                <span className="block truncate text-sm font-medium">
                                  {item.label}
                                </span>
                                <span className="block truncate text-xs text-slate-400 dark:text-slate-500">
                                  {item.hint}
                                </span>
                              </span>
                              {item.badge && (
                                <Badge tone={item.tone}>{item.badge}</Badge>
                              )}
                            </button>
                          </li>
                        );
                      })}
                    </ul>
                  </div>
                ))}
              </nav>

              {/* Footer / account */}
              <div className="border-t border-slate-200 p-3 dark:border-slate-800">
                <div className="flex items-center gap-3 rounded-2xl bg-slate-50 p-2.5 dark:bg-slate-800/60">
                  <span className="grid h-10 w-10 shrink-0 place-items-center rounded-full bg-gradient-to-br from-violet-500 to-indigo-500 text-sm font-semibold text-white">
                    PN
                  </span>
                  <div className="min-w-0 flex-1">
                    <p className="truncate text-sm font-medium text-slate-800 dark:text-slate-100">
                      Priya Nair
                    </p>
                    <p className="truncate text-xs text-slate-500 dark:text-slate-400">
                      priya@northwind.dev
                    </p>
                  </div>
                  <button
                    type="button"
                    onClick={close}
                    aria-label="Sign out"
                    className="grid h-9 w-9 shrink-0 place-items-center rounded-xl text-slate-500 transition hover:bg-white hover:text-rose-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-500 dark:text-slate-400 dark:hover:bg-slate-700 dark:hover:text-rose-300"
                  >
                    <LogoutIcon className="h-5 w-5" />
                  </button>
                </div>
              </div>
            </motion.div>
          </div>
        )}
      </AnimatePresence>
    </section>
  );
}

function Badge({
  children,
  tone = "amber",
}: {
  children: ReactNode;
  tone?: "amber" | "emerald";
}): ReactElement {
  if (tone === "emerald") {
    return (
      <span className="rounded-full bg-emerald-100 px-2 py-0.5 text-[11px] font-semibold text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300">
        {children}
      </span>
    );
  }
  return (
    <span className="sdl-badge-live rounded-full bg-amber-100 px-2 py-0.5 text-[11px] font-semibold text-amber-700 dark:bg-amber-500/15 dark:text-amber-300">
      {children}
    </span>
  );
}

function StatCard({
  label,
  value,
  delta,
  good = false,
}: {
  label: string;
  value: string;
  delta: string;
  good?: boolean;
}): ReactElement {
  return (
    <div className="rounded-2xl border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-900/60">
      <p className="text-xs font-medium uppercase tracking-wide text-slate-400 dark:text-slate-500">
        {label}
      </p>
      <div className="mt-1.5 flex items-baseline gap-2">
        <span className="text-2xl font-semibold tracking-tight text-slate-900 dark:text-white">
          {value}
        </span>
        <span
          className={[
            "text-xs font-semibold",
            good
              ? "text-emerald-600 dark:text-emerald-400"
              : "text-slate-500 dark:text-slate-400",
          ].join(" ")}
        >
          {delta}
        </span>
      </div>
    </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 →