Web InnoventixFreeCode

Compact Table

Original · free

dense compact table

byWeb InnoventixReact + Tailwind
tablecompacttables
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/table-compact.json
table-compact.tsx
"use client";

import {
  useEffect,
  useMemo,
  useRef,
  useState,
  type KeyboardEvent as ReactKeyboardEvent,
  type SVGProps,
} from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";

type Status = "running" | "degraded" | "provisioning" | "stopped";

type SortKey = "name" | "region" | "status" | "cpu" | "memory" | "cost" | "uptime";
type SortDir = "asc" | "desc";
type Density = "compact" | "cozy";

interface Instance {
  id: string;
  name: string;
  region: string;
  status: Status;
  cpu: number;
  memory: number;
  cost: number;
  uptime: number;
}

interface Column {
  key: SortKey;
  label: string;
  numeric: boolean;
}

const COLUMNS: Column[] = [
  { key: "name", label: "Instance", numeric: false },
  { key: "region", label: "Region", numeric: false },
  { key: "status", label: "Status", numeric: false },
  { key: "cpu", label: "CPU", numeric: true },
  { key: "memory", label: "Mem", numeric: true },
  { key: "cost", label: "$/mo", numeric: true },
  { key: "uptime", label: "Uptime", numeric: true },
];

const SEED: Instance[] = [
  { id: "i-01", name: "web-prod-01", region: "us-east-1", status: "running", cpu: 42, memory: 61, cost: 148, uptime: 214 },
  { id: "i-02", name: "web-prod-02", region: "us-east-1", status: "running", cpu: 38, memory: 57, cost: 148, uptime: 214 },
  { id: "i-03", name: "api-gateway-03", region: "eu-west-2", status: "degraded", cpu: 88, memory: 74, cost: 96, uptime: 132 },
  { id: "i-04", name: "worker-batch-07", region: "ap-south-1", status: "running", cpu: 71, memory: 82, cost: 64, uptime: 89 },
  { id: "i-05", name: "cache-redis-02", region: "us-east-1", status: "running", cpu: 24, memory: 91, cost: 52, uptime: 301 },
  { id: "i-06", name: "db-primary-01", region: "us-west-2", status: "running", cpu: 63, memory: 68, cost: 420, uptime: 512 },
  { id: "i-07", name: "db-replica-02", region: "us-west-2", status: "running", cpu: 55, memory: 66, cost: 210, uptime: 480 },
  { id: "i-08", name: "queue-nats-01", region: "eu-central-1", status: "provisioning", cpu: 12, memory: 20, cost: 44, uptime: 3 },
  { id: "i-09", name: "ml-infer-05", region: "ap-south-1", status: "degraded", cpu: 94, memory: 88, cost: 380, uptime: 47 },
  { id: "i-10", name: "cron-runner-01", region: "sa-east-1", status: "stopped", cpu: 0, memory: 0, cost: 18, uptime: 0 },
  { id: "i-11", name: "edge-cdn-11", region: "eu-west-2", status: "running", cpu: 33, memory: 40, cost: 72, uptime: 158 },
  { id: "i-12", name: "auth-svc-04", region: "us-east-1", status: "running", cpu: 47, memory: 52, cost: 88, uptime: 176 },
  { id: "i-13", name: "search-es-03", region: "eu-central-1", status: "degraded", cpu: 79, memory: 90, cost: 260, uptime: 133 },
  { id: "i-14", name: "backup-vault-01", region: "us-west-2", status: "stopped", cpu: 0, memory: 0, cost: 30, uptime: 0 },
  { id: "i-15", name: "gpu-train-02", region: "ap-south-1", status: "provisioning", cpu: 5, memory: 14, cost: 640, uptime: 1 },
  { id: "i-16", name: "metrics-tsdb-01", region: "sa-east-1", status: "running", cpu: 58, memory: 63, cost: 120, uptime: 205 },
];

const STATUS_RANK: Record<Status, number> = {
  running: 0,
  provisioning: 1,
  degraded: 2,
  stopped: 3,
};

const STATUS_META: Record<
  Status,
  { label: string; dot: string; chip: string; live: boolean }
> = {
  running: {
    label: "Running",
    dot: "bg-emerald-500",
    chip:
      "text-emerald-700 bg-emerald-50 ring-emerald-600/20 dark:text-emerald-300 dark:bg-emerald-400/10 dark:ring-emerald-400/20",
    live: true,
  },
  degraded: {
    label: "Degraded",
    dot: "bg-amber-500",
    chip:
      "text-amber-700 bg-amber-50 ring-amber-600/20 dark:text-amber-300 dark:bg-amber-400/10 dark:ring-amber-400/20",
    live: false,
  },
  provisioning: {
    label: "Provisioning",
    dot: "bg-sky-500",
    chip:
      "text-sky-700 bg-sky-50 ring-sky-600/20 dark:text-sky-300 dark:bg-sky-400/10 dark:ring-sky-400/20",
    live: false,
  },
  stopped: {
    label: "Stopped",
    dot: "bg-slate-400 dark:bg-slate-500",
    chip:
      "text-slate-600 bg-slate-100 ring-slate-500/20 dark:text-slate-400 dark:bg-slate-500/10 dark:ring-slate-400/20",
    live: false,
  },
};

const METER_TONE: Record<"low" | "mid" | "high", string> = {
  low: "bg-emerald-500",
  mid: "bg-amber-500",
  high: "bg-rose-500",
};

const usd = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
  maximumFractionDigits: 0,
});

function compareBy(a: Instance, b: Instance, key: SortKey): number {
  if (key === "name") return a.name.localeCompare(b.name);
  if (key === "region") return a.region.localeCompare(b.region);
  if (key === "status") return STATUS_RANK[a.status] - STATUS_RANK[b.status];
  const av = a[key];
  const bv = b[key];
  return typeof av === "number" && typeof bv === "number" ? av - bv : 0;
}

function SortGlyph({
  state,
  ...props
}: { state: SortDir | "none" } & SVGProps<SVGSVGElement>) {
  return (
    <svg
      viewBox="0 0 12 12"
      fill="none"
      aria-hidden="true"
      {...props}
    >
      <path
        d="M6 2.5v7M6 2.5 3.5 5M6 2.5 8.5 5"
        stroke="currentColor"
        strokeWidth="1.2"
        strokeLinecap="round"
        strokeLinejoin="round"
        className={
          state === "asc" ? "opacity-100" : "opacity-30"
        }
      />
      <path
        d="M6 9.5 3.5 7M6 9.5 8.5 7"
        stroke="currentColor"
        strokeWidth="1.2"
        strokeLinecap="round"
        strokeLinejoin="round"
        className={
          state === "desc" ? "opacity-100" : "opacity-30"
        }
      />
    </svg>
  );
}

function SearchIcon(props: SVGProps<SVGSVGElement>) {
  return (
    <svg viewBox="0 0 16 16" fill="none" aria-hidden="true" {...props}>
      <circle cx="7" cy="7" r="4.25" stroke="currentColor" strokeWidth="1.3" />
      <path
        d="m10.5 10.5 3 3"
        stroke="currentColor"
        strokeWidth="1.3"
        strokeLinecap="round"
      />
    </svg>
  );
}

function PlayIcon(props: SVGProps<SVGSVGElement>) {
  return (
    <svg viewBox="0 0 16 16" fill="none" aria-hidden="true" {...props}>
      <path
        d="M5 3.6v8.8L12.5 8 5 3.6Z"
        fill="currentColor"
      />
    </svg>
  );
}

function StopIcon(props: SVGProps<SVGSVGElement>) {
  return (
    <svg viewBox="0 0 16 16" fill="none" aria-hidden="true" {...props}>
      <rect x="4" y="4" width="8" height="8" rx="1.5" fill="currentColor" />
    </svg>
  );
}

function CloseIcon(props: SVGProps<SVGSVGElement>) {
  return (
    <svg viewBox="0 0 16 16" fill="none" aria-hidden="true" {...props}>
      <path
        d="m4.5 4.5 7 7m0-7-7 7"
        stroke="currentColor"
        strokeWidth="1.4"
        strokeLinecap="round"
      />
    </svg>
  );
}

function Meter({ value, dense }: { value: number; dense: boolean }) {
  const tone = value >= 85 ? "high" : value >= 60 ? "mid" : "low";
  return (
    <div className="flex items-center justify-end gap-2 tabular-nums">
      <span
        className={
          dense ? "w-8 text-right" : "w-9 text-right"
        }
      >
        {value}%
      </span>
      <span
        className="hidden h-1.5 w-14 overflow-hidden rounded-full bg-slate-200 dark:bg-slate-700 sm:block"
        aria-hidden="true"
      >
        <span
          className={`block h-full rounded-full ${METER_TONE[tone]}`}
          style={{ width: `${value}%` }}
        />
      </span>
    </div>
  );
}

export default function TableCompact() {
  const reduce = useReducedMotion();

  const [rows, setRows] = useState<Instance[]>(SEED);
  const [query, setQuery] = useState("");
  const [sort, setSort] = useState<{ key: SortKey; dir: SortDir }>({
    key: "cpu",
    dir: "desc",
  });
  const [selected, setSelected] = useState<Set<string>>(new Set());
  const [density, setDensity] = useState<Density>("compact");

  const selectAllRef = useRef<HTMLInputElement>(null);
  const densityBtns = useRef<Record<Density, HTMLButtonElement | null>>({
    compact: null,
    cozy: null,
  });

  const filtered = useMemo(() => {
    const q = query.trim().toLowerCase();
    const base = q
      ? rows.filter(
          (r) =>
            r.name.toLowerCase().includes(q) ||
            r.region.toLowerCase().includes(q) ||
            STATUS_META[r.status].label.toLowerCase().includes(q),
        )
      : rows;
    const dir = sort.dir === "asc" ? 1 : -1;
    return [...base].sort((a, b) => compareBy(a, b, sort.key) * dir);
  }, [rows, query, sort]);

  const visibleIds = useMemo(() => filtered.map((r) => r.id), [filtered]);
  const selectedVisible = useMemo(
    () => visibleIds.filter((id) => selected.has(id)),
    [visibleIds, selected],
  );
  const allSelected =
    filtered.length > 0 && selectedVisible.length === filtered.length;
  const someSelected = selectedVisible.length > 0 && !allSelected;

  useEffect(() => {
    if (selectAllRef.current) selectAllRef.current.indeterminate = someSelected;
  }, [someSelected]);

  const selectedRows = useMemo(
    () => rows.filter((r) => selected.has(r.id)),
    [rows, selected],
  );
  const costBasis = selectedRows.length > 0 ? selectedRows : filtered;
  const totalCost = costBasis.reduce((sum, r) => sum + r.cost, 0);

  function toggleSort(key: SortKey) {
    setSort((prev) =>
      prev.key === key
        ? { key, dir: prev.dir === "asc" ? "desc" : "asc" }
        : { key, dir: key === "name" || key === "region" ? "asc" : "desc" },
    );
  }

  function toggleRow(id: string) {
    setSelected((prev) => {
      const next = new Set(prev);
      if (next.has(id)) next.delete(id);
      else next.add(id);
      return next;
    });
  }

  function toggleAll() {
    setSelected((prev) => {
      const next = new Set(prev);
      if (allSelected) visibleIds.forEach((id) => next.delete(id));
      else visibleIds.forEach((id) => next.add(id));
      return next;
    });
  }

  function applyStatus(status: Status) {
    setRows((prev) =>
      prev.map((r) =>
        selected.has(r.id)
          ? status === "stopped"
            ? { ...r, status, cpu: 0, memory: 0 }
            : { ...r, status }
          : r,
      ),
    );
  }

  function onDensityKeyDown(e: ReactKeyboardEvent<HTMLDivElement>) {
    if (
      e.key === "ArrowRight" ||
      e.key === "ArrowDown" ||
      e.key === "ArrowLeft" ||
      e.key === "ArrowUp"
    ) {
      e.preventDefault();
      const next: Density = density === "compact" ? "cozy" : "compact";
      setDensity(next);
      requestAnimationFrame(() => densityBtns.current[next]?.focus());
    }
  }

  const cellPad = density === "compact" ? "px-3 py-1.5" : "px-4 py-3";
  const textSize = density === "compact" ? "text-xs" : "text-sm";
  const headPad = density === "compact" ? "px-3 py-2" : "px-4 py-3";
  const focusRing =
    "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900";

  const bulkTransition = reduce ? { duration: 0 } : { duration: 0.22, ease: [0.16, 1, 0.3, 1] as const };

  return (
    <section className="relative w-full overflow-hidden bg-gradient-to-b from-slate-50 to-white px-4 py-16 dark:from-slate-950 dark:to-slate-900 sm:px-6 sm:py-20">
      <style>{`
        @keyframes tcmp-ping {
          0% { transform: scale(1); opacity: 0.55; }
          75%, 100% { transform: scale(2.4); opacity: 0; }
        }
        @media (prefers-reduced-motion: reduce) {
          .tcmp-live { animation: none !important; }
        }
      `}</style>

      <div className="mx-auto w-full max-w-5xl">
        <header className="mb-6 flex flex-col gap-1">
          <p className="text-xs font-semibold uppercase tracking-[0.2em] text-indigo-600 dark:text-indigo-400">
            Fleet overview
          </p>
          <h2 className="text-2xl font-bold tracking-tight text-slate-900 dark:text-slate-50">
            Compute instances
          </h2>
          <p className="max-w-prose text-sm text-slate-500 dark:text-slate-400">
            {rows.length} instances across 6 regions. Sort any column, filter by
            name or status, and select rows to run bulk actions.
          </p>
        </header>

        {/* Toolbar */}
        <div className="mb-3 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
          <div className="relative w-full sm:max-w-xs">
            <label htmlFor="tcmp-search" className="sr-only">
              Filter instances
            </label>
            <SearchIcon className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400" />
            <input
              id="tcmp-search"
              type="text"
              inputMode="search"
              value={query}
              onChange={(e) => setQuery(e.target.value)}
              placeholder="Filter by name, region, status…"
              className={`w-full rounded-lg border border-slate-300 bg-white py-2 pl-9 pr-3 text-sm text-slate-900 placeholder:text-slate-400 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-100 dark:placeholder:text-slate-500 ${focusRing}`}
            />
          </div>

          <div
            role="radiogroup"
            aria-label="Row density"
            onKeyDown={onDensityKeyDown}
            className="inline-flex shrink-0 items-center gap-0.5 rounded-lg border border-slate-300 bg-slate-100 p-0.5 dark:border-slate-700 dark:bg-slate-800"
          >
            {(["compact", "cozy"] as const).map((opt) => {
              const active = density === opt;
              return (
                <button
                  key={opt}
                  ref={(el) => {
                    densityBtns.current[opt] = el;
                  }}
                  role="radio"
                  aria-checked={active}
                  tabIndex={active ? 0 : -1}
                  onClick={() => setDensity(opt)}
                  className={`rounded-md px-3 py-1 text-xs font-semibold capitalize transition-colors ${focusRing} ${
                    active
                      ? "bg-white text-slate-900 shadow-sm dark:bg-slate-950 dark:text-slate-50"
                      : "text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-200"
                  }`}
                >
                  {opt}
                </button>
              );
            })}
          </div>
        </div>

        {/* Table card */}
        <div className="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
          <div className="max-h-[28rem] overflow-auto">
            <table className={`w-full min-w-[46rem] border-collapse text-left ${textSize}`}>
              <caption className="sr-only">
                Compute instances. Column headers are buttons that sort the
                table. Use the checkboxes to select rows for bulk actions.
              </caption>
              <thead className="sticky top-0 z-10">
                <tr className="border-b border-slate-200 bg-slate-50/95 backdrop-blur dark:border-slate-800 dark:bg-slate-900/95">
                  <th scope="col" className={`${headPad} w-10`}>
                    <input
                      ref={selectAllRef}
                      type="checkbox"
                      checked={allSelected}
                      onChange={toggleAll}
                      aria-label="Select all visible rows"
                      className={`h-4 w-4 cursor-pointer rounded border-slate-300 accent-indigo-600 dark:border-slate-600 ${focusRing}`}
                    />
                  </th>
                  {COLUMNS.map((col) => {
                    const isSorted = sort.key === col.key;
                    const ariaSort = isSorted
                      ? sort.dir === "asc"
                        ? "ascending"
                        : "descending"
                      : "none";
                    return (
                      <th
                        key={col.key}
                        scope="col"
                        aria-sort={ariaSort}
                        className={`${headPad} font-semibold ${
                          col.numeric ? "text-right" : "text-left"
                        }`}
                      >
                        <button
                          type="button"
                          onClick={() => toggleSort(col.key)}
                          className={`group inline-flex items-center gap-1.5 rounded uppercase tracking-wide ${
                            col.numeric ? "flex-row-reverse" : ""
                          } ${
                            isSorted
                              ? "text-slate-900 dark:text-slate-100"
                              : "text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-200"
                          } ${focusRing}`}
                        >
                          <span>{col.label}</span>
                          <SortGlyph
                            state={isSorted ? sort.dir : "none"}
                            className="h-3 w-3 shrink-0"
                          />
                        </button>
                      </th>
                    );
                  })}
                </tr>
              </thead>
              <tbody>
                {filtered.map((r) => {
                  const meta = STATUS_META[r.status];
                  const isSel = selected.has(r.id);
                  return (
                    <tr
                      key={r.id}
                      data-selected={isSel}
                      className={`border-b border-slate-100 transition-colors last:border-0 dark:border-slate-800/70 ${
                        isSel
                          ? "bg-indigo-50/70 dark:bg-indigo-500/10"
                          : "hover:bg-slate-50 dark:hover:bg-slate-800/40"
                      }`}
                    >
                      <td className={cellPad}>
                        <input
                          type="checkbox"
                          checked={isSel}
                          onChange={() => toggleRow(r.id)}
                          aria-label={`Select ${r.name}`}
                          className={`h-4 w-4 cursor-pointer rounded border-slate-300 accent-indigo-600 dark:border-slate-600 ${focusRing}`}
                        />
                      </td>
                      <td className={`${cellPad} font-medium text-slate-900 dark:text-slate-100`}>
                        <span className="font-mono">{r.name}</span>
                      </td>
                      <td className={`${cellPad} text-slate-500 dark:text-slate-400`}>
                        <span className="font-mono">{r.region}</span>
                      </td>
                      <td className={cellPad}>
                        <span
                          className={`inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs font-medium ring-1 ring-inset ${meta.chip}`}
                        >
                          <span className="relative flex h-1.5 w-1.5">
                            {meta.live && !reduce ? (
                              <span
                                className={`tcmp-live absolute inline-flex h-full w-full rounded-full ${meta.dot}`}
                                style={{
                                  animation:
                                    "tcmp-ping 1.8s cubic-bezier(0,0,0.2,1) infinite",
                                }}
                              />
                            ) : null}
                            <span
                              className={`relative inline-flex h-1.5 w-1.5 rounded-full ${meta.dot}`}
                            />
                          </span>
                          {meta.label}
                        </span>
                      </td>
                      <td className={`${cellPad} text-slate-700 dark:text-slate-300`}>
                        <Meter value={r.cpu} dense={density === "compact"} />
                      </td>
                      <td className={`${cellPad} text-slate-700 dark:text-slate-300`}>
                        <Meter value={r.memory} dense={density === "compact"} />
                      </td>
                      <td className={`${cellPad} text-right font-medium tabular-nums text-slate-900 dark:text-slate-100`}>
                        {usd.format(r.cost)}
                      </td>
                      <td className={`${cellPad} text-right tabular-nums text-slate-500 dark:text-slate-400`}>
                        {r.uptime > 0 ? `${r.uptime}d` : "—"}
                      </td>
                    </tr>
                  );
                })}
                {filtered.length === 0 ? (
                  <tr>
                    <td
                      colSpan={COLUMNS.length + 1}
                      className="px-4 py-14 text-center text-sm text-slate-500 dark:text-slate-400"
                    >
                      No instances match{" "}
                      <span className="font-mono font-medium text-slate-700 dark:text-slate-300">
                        “{query}”
                      </span>
                      .
                    </td>
                  </tr>
                ) : null}
              </tbody>
            </table>
          </div>

          {/* Footer summary */}
          <div className="flex flex-wrap items-center justify-between gap-x-6 gap-y-2 border-t border-slate-200 bg-slate-50/60 px-4 py-2.5 text-xs text-slate-500 dark:border-slate-800 dark:bg-slate-900/60 dark:text-slate-400">
            <span aria-live="polite">
              <span className="font-semibold text-slate-700 dark:text-slate-200">
                {selected.size}
              </span>{" "}
              selected · {filtered.length} shown of {rows.length}
            </span>
            <span>
              {selectedRows.length > 0 ? "Selected" : "Visible"} spend{" "}
              <span className="font-semibold tabular-nums text-slate-700 dark:text-slate-200">
                {usd.format(totalCost)}
              </span>
              /mo
            </span>
          </div>
        </div>

        {/* Bulk action bar */}
        <div className="pointer-events-none fixed inset-x-0 bottom-4 z-20 flex justify-center px-4">
          <AnimatePresence>
            {selected.size > 0 ? (
              <motion.div
                initial={reduce ? false : { opacity: 0, y: 16 }}
                animate={{ opacity: 1, y: 0 }}
                exit={reduce ? { opacity: 0 } : { opacity: 0, y: 16 }}
                transition={bulkTransition}
                role="region"
                aria-label="Bulk actions"
                className="pointer-events-auto flex items-center gap-2 rounded-xl border border-slate-700 bg-slate-900 px-3 py-2 text-sm text-slate-100 shadow-xl dark:border-slate-600 dark:bg-slate-800"
              >
                <span className="px-1 text-xs font-semibold text-slate-300">
                  {selected.size} selected
                </span>
                <span className="h-4 w-px bg-slate-700" aria-hidden="true" />
                <button
                  type="button"
                  onClick={() => applyStatus("running")}
                  className={`inline-flex items-center gap-1.5 rounded-lg bg-emerald-500 px-2.5 py-1.5 text-xs font-semibold text-white hover:bg-emerald-400 ${focusRing} focus-visible:ring-offset-slate-900`}
                >
                  <PlayIcon className="h-3.5 w-3.5" />
                  Start
                </button>
                <button
                  type="button"
                  onClick={() => applyStatus("stopped")}
                  className={`inline-flex items-center gap-1.5 rounded-lg bg-rose-500 px-2.5 py-1.5 text-xs font-semibold text-white hover:bg-rose-400 ${focusRing} focus-visible:ring-offset-slate-900`}
                >
                  <StopIcon className="h-3.5 w-3.5" />
                  Stop
                </button>
                <button
                  type="button"
                  onClick={() => setSelected(new Set())}
                  aria-label="Clear selection"
                  className={`inline-flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-xs font-medium text-slate-300 hover:bg-slate-800 hover:text-white dark:hover:bg-slate-700 ${focusRing} focus-visible:ring-offset-slate-900`}
                >
                  <CloseIcon className="h-3.5 w-3.5" />
                  Clear
                </button>
              </motion.div>
            ) : null}
          </AnimatePresence>
        </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 →