Compact Table
Original · freedense compact table
byWeb InnoventixReact + Tailwind
tablecompacttables
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.jsontable-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 quoteSimilar components
Browse all →
Basic Table
Originalclean basic data table

Striped Table
Originalstriped-row table

Bordered Table
Originalbordered table

Hover Table
Originalrow-hover highlight table

Sortable Table
Originalsortable-column table

Selectable Table
Originaltable with row checkboxes and select-all

Sticky Header Table
Originalsticky header scroll table

Pagination Table
Originaltable with footer pagination

Search Table
Originaltable with a search filter

Expandable Table
Originaltable with expandable rows

Actions Table
Originaltable with row action menus

Status Badges Table
Originaltable with status badges

