Status tables
Five tables whose status cells carry semantic meaning: the status text maps deterministically to an info, success, warning or danger tone, and the cell exposes it as data-tone. Badge, dot, full row tone, progress bar and icon treatments.
Installation
The styled layer is free and installs like any other ai2 component.
Run the following command
npx shadcn@latest add @ai2/table-statusDependencies, the @ai2/tokens theme and the component file are installed together.
Install dependencies
npm install lucide-reactCopy the source
components/ui/table-status.tsx"use client"
import type * as React from "react"
import { AlertTriangle, CheckCircle2, Clock, Info, XCircle } from "lucide-react"
import { cn } from "@/lib/utils"
/* Status table family: 5 tables with status cells. The status text maps to a
semantic tone (info/success/warning/danger) and the cell carries data-tone: a
badge, a dot, a full tone background, a progress bar and an icon. Color comes ONLY
from semantic tokens - the tone tokens (-foreground/-soft/-soft-foreground) and
alpha color-mix. Deterministic, renders with no props too. */
export type StyledSize = "sm" | "md" | "lg" | "xl"
type Column = { key: string; label: React.ReactNode }
type Row = Record<string, React.ReactNode>
type TableProps = {
className?: string
size?: StyledSize
columns?: Column[]
rows?: Row[]
}
type Tone = "neutral" | "info" | "success" | "warning" | "danger"
/* A sensible default data set so it renders without props. */
const defaultColumns: Column[] = [
{ key: "name", label: "Name" },
{ key: "role", label: "Role" },
{ key: "status", label: "Status" },
]
const defaultRows: Row[] = [
{ name: "Ada Lovelace", role: "Engineer", status: "Active" },
{ name: "Alan Turing", role: "Researcher", status: "Pending" },
{ name: "Grace Hopper", role: "Architect", status: "Failed" },
{ name: "Katherine Johnson", role: "Analyst", status: "Queued" },
]
/* An extra column plus deterministic percentages for the progress variant. */
const progressColumns: Column[] = [
{ key: "name", label: "Name" },
{ key: "status", label: "Status" },
{ key: "progress", label: "Progress" },
]
const progressRows: Row[] = [
{ name: "Ada Lovelace", status: "Active", progress: "92" },
{ name: "Alan Turing", status: "Pending", progress: "58" },
{ name: "Grace Hopper", status: "Failed", progress: "24" },
{ name: "Katherine Johnson", status: "Queued", progress: "71" },
]
/* Hucre yogunlugu: padding + metin olcegi. */
const cell: Record<StyledSize, string> = {
sm: "px-2.5 py-1.5 text-xs",
md: "px-3 py-2 text-sm",
lg: "px-4 py-2.5 text-sm",
xl: "px-5 py-3 text-base",
}
const headCell: Record<StyledSize, string> = {
sm: "px-2.5 py-1.5 text-xs",
md: "px-3 py-2 text-xs",
lg: "px-4 py-2.5 text-sm",
xl: "px-5 py-3 text-sm",
}
const tableBase = "w-full border-collapse text-left align-middle text-foreground"
const headBase = "font-medium text-muted-foreground"
function resolve(columns: Column[] | undefined, rows: Row[] | undefined, fbCols: Column[], fbRows: Row[]) {
return {
cols: columns && columns.length > 0 ? columns : fbCols,
data: rows && rows.length > 0 ? rows : fbRows,
}
}
/* Durum metnini semantik tona esler (deterministik, buyuk/kucuk harf duyarsiz). */
const toneByStatus: Record<string, Tone> = {
active: "success",
done: "success",
complete: "success",
pending: "warning",
away: "warning",
failed: "danger",
error: "danger",
blocked: "danger",
queued: "info",
running: "info",
}
function toText(value: React.ReactNode): string {
if (typeof value === "string") return value
if (typeof value === "number") return String(value)
return ""
}
function toneOf(value: React.ReactNode): Tone {
return toneByStatus[toText(value).trim().toLowerCase()] ?? "neutral"
}
/* Yumusak ton yuzeyleri (rozet / zemin). */
const softTone: Record<Tone, string> = {
neutral: "bg-muted text-muted-foreground border-border",
info: "bg-info-soft text-info-soft-foreground border-info/40",
success: "bg-success-soft text-success-soft-foreground border-success/40",
warning: "bg-warning-soft text-warning-soft-foreground border-warning/40",
danger: "bg-danger-soft text-danger-soft-foreground border-danger/40",
}
/* Dolu ton yuzeyleri. */
const solidTone: Record<Tone, string> = {
neutral: "bg-muted text-muted-foreground",
info: "bg-info text-info-foreground",
success: "bg-success text-success-foreground",
warning: "bg-warning text-warning-foreground",
danger: "bg-danger text-danger-foreground",
}
/* Nokta / cubuk dolgusu. */
const dotTone: Record<Tone, string> = {
neutral: "bg-muted-foreground",
info: "bg-info",
success: "bg-success",
warning: "bg-warning",
danger: "bg-danger",
}
/* Metin tonu. */
const textTone: Record<Tone, string> = {
neutral: "text-muted-foreground",
info: "text-info",
success: "text-success",
warning: "text-warning",
danger: "text-danger",
}
const iconTone: Record<Tone, React.ReactNode> = {
neutral: <Info />,
info: <Clock />,
success: <CheckCircle2 />,
warning: <AlertTriangle />,
danger: <XCircle />,
}
const iconBox =
"inline-flex items-center gap-1.5 [&_svg]:size-4 [&_svg]:shrink-0 [&>svg]:shrink-0 [&_i]:text-base [&_i]:leading-none [&>i]:shrink-0"
/* Badge: durum, ton renkli yumusak bir rozet olarak okunur. */
export function BadgeTable({ className, size = "md", columns, rows }: TableProps) {
const { cols, data } = resolve(columns, rows, defaultColumns, defaultRows)
return (
<div
data-slot="styled-table"
className={cn("w-full overflow-x-auto rounded-lg border border-border", className)}
>
<table className={tableBase}>
<thead>
<tr className="border-b border-border bg-muted">
{cols.map((col) => (
<th key={col.key} scope="col" className={cn(headBase, headCell[size])}>
{col.label}
</th>
))}
</tr>
</thead>
<tbody>
{data.map((row, i) => {
const tone = toneOf(row.status)
return (
<tr key={i} className="border-b border-border/60 last:border-b-0">
{cols.map((col) =>
col.key === "status" ? (
<td key={col.key} data-tone={tone} className={cell[size]}>
<span
className={cn(
"inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium",
softTone[tone]
)}
>
{row[col.key]}
</span>
</td>
) : (
<td key={col.key} className={cell[size]}>
{row[col.key]}
</td>
)
)}
</tr>
)
})}
</tbody>
</table>
</div>
)
}
/* Dot: the status is shown with a small tone dot + plain text. */
export function DotTable({ className, size = "md", columns, rows }: TableProps) {
const { cols, data } = resolve(columns, rows, defaultColumns, defaultRows)
return (
<div
data-slot="styled-table"
className={cn("w-full overflow-x-auto rounded-lg border border-border", className)}
>
<table className={tableBase}>
<thead>
<tr className="border-b border-border">
{cols.map((col) => (
<th key={col.key} scope="col" className={cn(headBase, headCell[size])}>
{col.label}
</th>
))}
</tr>
</thead>
<tbody>
{data.map((row, i) => {
const tone = toneOf(row.status)
return (
<tr key={i} className="border-b border-border/60 last:border-b-0">
{cols.map((col) =>
col.key === "status" ? (
<td key={col.key} data-tone={tone} className={cell[size]}>
<span className="inline-flex items-center gap-2">
<span
aria-hidden="true"
className={cn("size-2 shrink-0 rounded-full", dotTone[tone])}
/>
{row[col.key]}
</span>
</td>
) : (
<td key={col.key} className={cell[size]}>
{row[col.key]}
</td>
)
)}
</tr>
)
})}
</tbody>
</table>
</div>
)
}
/* Tone: the whole row is painted with the soft tone of the state, and the state cell takes the solid tone. */
export function ToneTable({ className, size = "md", columns, rows }: TableProps) {
const { cols, data } = resolve(columns, rows, defaultColumns, defaultRows)
return (
<div
data-slot="styled-table"
className={cn("w-full overflow-x-auto rounded-lg border border-border", className)}
>
<table className={tableBase}>
<thead>
<tr className="border-b border-border bg-muted">
{cols.map((col) => (
<th key={col.key} scope="col" className={cn(headBase, headCell[size])}>
{col.label}
</th>
))}
</tr>
</thead>
<tbody>
{data.map((row, i) => {
const tone = toneOf(row.status)
return (
<tr
key={i}
data-tone={tone}
className={cn("border-b border-border/60 last:border-b-0", softTone[tone])}
>
{cols.map((col) =>
col.key === "status" ? (
<td key={col.key} data-tone={tone} className={cell[size]}>
<span
className={cn(
"inline-flex items-center rounded-md px-2 py-0.5 text-xs font-semibold",
solidTone[tone]
)}
>
{row[col.key]}
</span>
</td>
) : (
<td key={col.key} className={cell[size]}>
{row[col.key]}
</td>
)
)}
</tr>
)
})}
</tbody>
</table>
</div>
)
}
/* Progress: durum tonuyla dolan bir ilerleme cubugu kolonu. */
export function ProgressTable({ className, size = "md", columns, rows }: TableProps) {
const { cols, data } = resolve(columns, rows, progressColumns, progressRows)
return (
<div
data-slot="styled-table"
className={cn("w-full overflow-x-auto rounded-lg border border-border", className)}
>
<table className={tableBase}>
<thead>
<tr className="border-b border-border bg-muted">
{cols.map((col) => (
<th key={col.key} scope="col" className={cn(headBase, headCell[size])}>
{col.label}
</th>
))}
</tr>
</thead>
<tbody>
{data.map((row, i) => {
const tone = toneOf(row.status)
const raw = Number(toText(row.progress))
const pct = Number.isNaN(raw) ? 0 : Math.min(100, Math.max(0, raw))
return (
<tr key={i} className="border-b border-border/60 last:border-b-0">
{cols.map((col) => {
if (col.key === "progress") {
return (
<td key={col.key} data-tone={tone} className={cn(cell[size], "min-w-40")}>
<span className="flex items-center gap-2">
<span
role="progressbar"
aria-valuenow={pct}
aria-valuemin={0}
aria-valuemax={100}
aria-label={`Progress ${pct} percent`}
className="h-1.5 w-full overflow-hidden rounded-full bg-[color-mix(in_oklab,var(--color-foreground)_10%,transparent)]"
>
<span
className={cn("block h-full rounded-full", dotTone[tone])}
style={{ width: `${pct}%` }}
/>
</span>
<span className="w-9 shrink-0 text-right text-xs tabular-nums text-muted-foreground">
{`${pct}%`}
</span>
</span>
</td>
)
}
if (col.key === "status") {
return (
<td key={col.key} data-tone={tone} className={cn(cell[size], textTone[tone])}>
{row[col.key]}
</td>
)
}
return (
<td key={col.key} className={cell[size]}>
{row[col.key]}
</td>
)
})}
</tr>
)
})}
</tbody>
</table>
</div>
)
}
/* Icon: the status is marked with a lucide icon matching the tone + colored text. */
export function IconTable({ className, size = "md", columns, rows }: TableProps) {
const { cols, data } = resolve(columns, rows, defaultColumns, defaultRows)
return (
<div
data-slot="styled-table"
className={cn("w-full overflow-x-auto rounded-lg border border-border", className)}
>
<table className={tableBase}>
<thead>
<tr className="border-b border-border bg-muted">
{cols.map((col) => (
<th key={col.key} scope="col" className={cn(headBase, headCell[size])}>
{col.label}
</th>
))}
</tr>
</thead>
<tbody>
{data.map((row, i) => {
const tone = toneOf(row.status)
return (
<tr key={i} className="border-b border-border/60 last:border-b-0">
{cols.map((col) =>
col.key === "status" ? (
<td key={col.key} data-tone={tone} className={cell[size]}>
<span className={cn(iconBox, textTone[tone], "font-medium")}>
{iconTone[tone]}
{row[col.key]}
</span>
</td>
) : (
<td key={col.key} className={cell[size]}>
{row[col.key]}
</td>
)
)}
</tr>
)
})}
</tbody>
</table>
</div>
)
}Manual installs skip the @ai2/tokens theme, so add the token CSS from the theming guide or the tone colors will be missing.
Variations
5 takes on the same idea. Each is its own export, and every one accepts a size prop (sm, md, lg, xl) aligned to the base Button scale.
Badge
The status reads as a soft, tone-colored badge.
| Name | Role | Status |
|---|---|---|
| Ada Lovelace | Engineer | Active |
| Alan Turing | Researcher | Pending |
| Grace Hopper | Architect | Failed |
| Katherine Johnson | Analyst | Queued |
import { BadgeTable } from "@/components/ui/table-status"
<BadgeTable />| Name | Role | Status |
|---|---|---|
| Ada Lovelace | Engineer | Active |
| Alan Turing | Researcher | Pending |
| Grace Hopper | Architect | Failed |
| Katherine Johnson | Analyst | Queued |
| Name | Role | Status |
|---|---|---|
| Ada Lovelace | Engineer | Active |
| Alan Turing | Researcher | Pending |
| Grace Hopper | Architect | Failed |
| Katherine Johnson | Analyst | Queued |
| Name | Role | Status |
|---|---|---|
| Ada Lovelace | Engineer | Active |
| Alan Turing | Researcher | Pending |
| Grace Hopper | Architect | Failed |
| Katherine Johnson | Analyst | Queued |
| Name | Role | Status |
|---|---|---|
| Ada Lovelace | Engineer | Active |
| Alan Turing | Researcher | Pending |
| Grace Hopper | Architect | Failed |
| Katherine Johnson | Analyst | Queued |
Dot
A small tone dot next to plain status text.
| Name | Role | Status |
|---|---|---|
| Ada Lovelace | Engineer | Active |
| Alan Turing | Researcher | Pending |
| Grace Hopper | Architect | Failed |
| Katherine Johnson | Analyst | Queued |
import { DotTable } from "@/components/ui/table-status"
<DotTable />| Name | Role | Status |
|---|---|---|
| Ada Lovelace | Engineer | Active |
| Alan Turing | Researcher | Pending |
| Grace Hopper | Architect | Failed |
| Katherine Johnson | Analyst | Queued |
| Name | Role | Status |
|---|---|---|
| Ada Lovelace | Engineer | Active |
| Alan Turing | Researcher | Pending |
| Grace Hopper | Architect | Failed |
| Katherine Johnson | Analyst | Queued |
| Name | Role | Status |
|---|---|---|
| Ada Lovelace | Engineer | Active |
| Alan Turing | Researcher | Pending |
| Grace Hopper | Architect | Failed |
| Katherine Johnson | Analyst | Queued |
| Name | Role | Status |
|---|---|---|
| Ada Lovelace | Engineer | Active |
| Alan Turing | Researcher | Pending |
| Grace Hopper | Architect | Failed |
| Katherine Johnson | Analyst | Queued |
Tone
The whole row takes the soft tone; the status cell goes solid.
| Name | Role | Status |
|---|---|---|
| Ada Lovelace | Engineer | Active |
| Alan Turing | Researcher | Pending |
| Grace Hopper | Architect | Failed |
| Katherine Johnson | Analyst | Queued |
import { ToneTable } from "@/components/ui/table-status"
<ToneTable />| Name | Role | Status |
|---|---|---|
| Ada Lovelace | Engineer | Active |
| Alan Turing | Researcher | Pending |
| Grace Hopper | Architect | Failed |
| Katherine Johnson | Analyst | Queued |
| Name | Role | Status |
|---|---|---|
| Ada Lovelace | Engineer | Active |
| Alan Turing | Researcher | Pending |
| Grace Hopper | Architect | Failed |
| Katherine Johnson | Analyst | Queued |
| Name | Role | Status |
|---|---|---|
| Ada Lovelace | Engineer | Active |
| Alan Turing | Researcher | Pending |
| Grace Hopper | Architect | Failed |
| Katherine Johnson | Analyst | Queued |
| Name | Role | Status |
|---|---|---|
| Ada Lovelace | Engineer | Active |
| Alan Turing | Researcher | Pending |
| Grace Hopper | Architect | Failed |
| Katherine Johnson | Analyst | Queued |
Progress
A progress bar column filled with the status tone.
| Name | Status | Progress |
|---|---|---|
| Ada Lovelace | Active | 92% |
| Alan Turing | Pending | 58% |
| Grace Hopper | Failed | 24% |
| Katherine Johnson | Queued | 71% |
import { ProgressTable } from "@/components/ui/table-status"
<ProgressTable />| Name | Status | Progress |
|---|---|---|
| Ada Lovelace | Active | 92% |
| Alan Turing | Pending | 58% |
| Grace Hopper | Failed | 24% |
| Katherine Johnson | Queued | 71% |
| Name | Status | Progress |
|---|---|---|
| Ada Lovelace | Active | 92% |
| Alan Turing | Pending | 58% |
| Grace Hopper | Failed | 24% |
| Katherine Johnson | Queued | 71% |
| Name | Status | Progress |
|---|---|---|
| Ada Lovelace | Active | 92% |
| Alan Turing | Pending | 58% |
| Grace Hopper | Failed | 24% |
| Katherine Johnson | Queued | 71% |
| Name | Status | Progress |
|---|---|---|
| Ada Lovelace | Active | 92% |
| Alan Turing | Pending | 58% |
| Grace Hopper | Failed | 24% |
| Katherine Johnson | Queued | 71% |
Icon
A lucide icon plus tone-colored text marks the status.
| Name | Role | Status |
|---|---|---|
| Ada Lovelace | Engineer | Active |
| Alan Turing | Researcher | Pending |
| Grace Hopper | Architect | Failed |
| Katherine Johnson | Analyst | Queued |
import { IconTable } from "@/components/ui/table-status"
<IconTable />| Name | Role | Status |
|---|---|---|
| Ada Lovelace | Engineer | Active |
| Alan Turing | Researcher | Pending |
| Grace Hopper | Architect | Failed |
| Katherine Johnson | Analyst | Queued |
| Name | Role | Status |
|---|---|---|
| Ada Lovelace | Engineer | Active |
| Alan Turing | Researcher | Pending |
| Grace Hopper | Architect | Failed |
| Katherine Johnson | Analyst | Queued |
| Name | Role | Status |
|---|---|---|
| Ada Lovelace | Engineer | Active |
| Alan Turing | Researcher | Pending |
| Grace Hopper | Architect | Failed |
| Katherine Johnson | Analyst | Queued |
| Name | Role | Status |
|---|---|---|
| Ada Lovelace | Engineer | Active |
| Alan Turing | Researcher | Pending |
| Grace Hopper | Architect | Failed |
| Katherine Johnson | Analyst | Queued |
ai2 Status tables: 5 styled variations on the token system
The ai2 Status tables are a set of 5 decorative button variations from the styled layer of the @ai2 design system, built around data tables with semantic, tone-driven status cells. They are free and MIT licensed, and every color comes from a semantic token, so they theme with the rest of ai2 in light and dark.
Motion runs on framer-motion: there is no animation; the tones are static token surfaces. The styled layer is opt-in, so the dependency only lands if you use it; the base components stay lean. Under reduced motion, nothing changes, since the tables do not animate.
What is in the ai2 Status tables?
5 exports in one file: Badge, Dot, Tone, Progress and Icon. Each renders a native button and takes a size prop (sm, md, lg, xl) aligned to the base Button. They are separate from the base Button on purpose: the base keeps its clean variant, tone and size axes, while the styled layer carries the effects.
You own the file. Copy the one category file and you have all 5 variations, with no runtime dependency on ai2 itself.
Why use it
- On-system by construction: Every color resolves to an ai2 semantic token, so the buttons follow your theme in light and dark with no extra work.
- Effect without the sprawl: The decorations live in a dedicated styled file, so the base Button keeps its clean, predictable API.
- Accessible and honest: Each renders a real button element, keeps a visible focus ring, and respects prefers-reduced-motion.
Features
- Token-driven color: No hardcoded hex or oklch; the look recolors with your theme tokens.
- framer-motion: there is no animation; the tones are static token surfaces.
- Reduced-motion aware: Under prefers-reduced-motion, nothing changes, since the tables do not animate.
- Size aligned to the base: Every variation takes sm, md, lg and xl matching the base Button height scale, so styled and base buttons line up in a row.
Production tips
- Use it for emphasis, not everywhere: Styled buttons draw the eye. Reserve them for the one action you want people to take on a screen, and use the base Button for the rest.
- Keep labels as verbs: The decoration adds weight, so a clear action label keeps the button scannable.
- Pick one variation per surface: The variations share a family; using two different ones in the same view competes for attention.
Works with the rest of ai2
The Status tables sit alongside the base Button and the rest of the @ai2 registry. They share the same token file, so a styled action next to a base button or a badge stays visually consistent in both modes.