Filter Table

Data & Tables

Status chips that reorganize live data.

Interactive Live Preview
components/ui/filter-table.tsxTypeScript · React 19
1"use client";
2
3import { useState } from "react";
4
5/* ─────────────────────────────────────────────────────────
6 * FILTER TABLE
7 * Status chips directly filter the task table.
8 * ───────────────────────────────────────────────────────── */
9
10type Status = "todo" | "progress" | "done";
11
12const FILTERS: { key: "all" | Status; label: string; dot?: string; count: number }[] = [
13 { key: "all", label: "All", count: 5 },
14 { key: "todo", label: "To do", dot: "#f09a2f", count: 2 },
15 { key: "progress", label: "In Progress", dot: "#16a6c7", count: 2 },
16 { key: "done", label: "Completed", dot: "#25a878", count: 1 },
17];
18
19const ROWS: { task: string; date: string; status: Status; owner: string }[] = [
20 { task: "Restock mango sorbet", date: "Dec 03", status: "todo", owner: "Mango Moon Gelato" },
21 { task: "Churn black sesame", date: "Sep 22", status: "progress", owner: "Kumo Creamery" },
22 { task: "Print summer menu", date: "Jan 02", status: "todo", owner: "Coral Coast Sorbet" },
23 { task: "Taste-test batch 42", date: "Nov 08", status: "progress", owner: "Maple Orbit" },
24 { task: "Order waffle cones", date: "Apr 14", status: "done", owner: "Aurora Scoops" },
25];
26
27const PILLS: Record<Status, { label: string; cls: string }> = {
28 todo: { label: "To do", cls: "filter-status-todo" },
29 progress: { label: "In Progress", cls: "filter-status-progress" },
30 done: { label: "Completed", cls: "filter-status-done" },
31};
32
33export default function FilterTable() {
34 const [filter, setFilter] = useState<"all" | Status>("all");
35
36 return (
37 <div className="w-full max-w-105">
38 {/* filter chips */}
39 <div
40 className="-mx-1 mb-1 flex items-center gap-1 overflow-x-auto px-1 py-1"
41 style={{ scrollbarWidth: "none" }}
42 >
43 {FILTERS.map((f) => {
44 const active = filter === f.key;
45 return (
46 <button
47 key={f.key}
48 type="button"
49 aria-pressed={active}
50 onClick={() => setFilter(f.key)}
51 className={`flex h-6.5 shrink-0 items-center gap-1.5 rounded-full px-2.5 text-[12px]
52 font-medium transition-[background-color,box-shadow,color] duration-200
53 ${active ? "bg-surface text-ink shadow-btn" : "text-ink-2 hover:bg-hover"}`}
54 >
55 {f.dot && <span className="size-1.5 rounded-full" style={{ background: f.dot }} />}
56 {f.label}
57 <span
58 className={`rounded-[4px] px-1 text-[10.5px] tabular-nums
59 ${active ? "bg-field text-ink-2" : "text-ink-3"}`}
60 >
61 {f.count}
62 </span>
63 </button>
64 );
65 })}
66 </div>
67
68 {/* table */}
69 <div
70 aria-label="Scrollable task table"
71 className="overflow-x-auto rounded-card bg-surface shadow-card"
72 role="region"
73 tabIndex={0}
74 style={{ scrollbarWidth: "none" }}
75 >
76 <div className="min-w-[420px]">
77 <div className="grid grid-cols-[1.3fr_0.6fr_0.95fr_0.9fr] gap-x-2 border-b border-line px-3 py-2 text-[12.5px] font-medium text-ink-3">
78 <span>Task name</span>
79 <span>Date</span>
80 <span>Status</span>
81 <span>Advisor</span>
82 </div>
83 {ROWS.map((row) => {
84 const shown = filter === "all" || row.status === filter;
85 const pill = PILLS[row.status];
86 return (
87 <div
88 key={row.task}
89 className="grid transition-[grid-template-rows,opacity] duration-300"
90 style={{
91 gridTemplateRows: shown ? "1fr" : "0fr",
92 opacity: shown ? 1 : 0,
93 transitionTimingFunction: "cubic-bezier(0.23, 1, 0.32, 1)",
94 }}
95 >
96 <div className="overflow-hidden">
97 <div
98 className="grid grid-cols-[1.3fr_0.6fr_0.95fr_0.9fr] gap-x-2 items-center border-b
99 border-line px-3 py-2 text-[13px] transition-colors duration-100
100 last:border-0 hover:bg-hover"
101 >
102 <span className="truncate font-medium text-ink">{row.task}</span>
103 <span className="text-ink-2 tabular-nums">{row.date}</span>
104 <span>
105 <span
106 className={`inline-flex h-[23px] items-center rounded-[8px] border px-[7px]
107 text-[13px] font-medium ${pill.cls}`}
108 >
109 {pill.label}
110 </span>
111 </span>
112 <span className="truncate text-ink-2">{row.owner}</span>
113 </div>
114 </div>
115 </div>
116 );
117 })}
118 </div>
119 </div>
120 </div>
121 );
122}
123

Props & Specification

PropTypeDefaultDescription
variantstring&quot;default&quot;Visual state and layout presentation mode.
classNamestring""Optional Tailwind CSS wrapper class overrides.

Related Components in Data & Tables