Patterns
Status
Three components that show the state of a record with a tone and a label.
Patterns
Three components that show the state of a record with a tone and a label.
| Invoice | Customer | Status |
|---|---|---|
| INV-2041 | Acme | Paid |
| INV-2042 | Globex | Open |
| INV-2043 | Northwind Traders | Overdue |
import { StatusBadge, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, type StatusTone } from 'ferry-ui'
type InvoiceStatus = 'paid' | 'open' | 'overdue'
// One map from the statuses of your domain to a tone and a label.
const INVOICE_STATUS: Record<InvoiceStatus, { tone: StatusTone; label: string }> = {
paid: { tone: 'success', label: 'Paid' },
open: { tone: 'info', label: 'Open' },
overdue: { tone: 'destructive', label: 'Overdue' },
}
const INVOICES: { number: string; customer: string; status: InvoiceStatus }[] = [
{ number: 'INV-2041', customer: 'Acme', status: 'paid' },
{ number: 'INV-2042', customer: 'Globex', status: 'open' },
{ number: 'INV-2043', customer: 'Northwind Traders', status: 'overdue' },
]
export default function StatusHero() {
return (
<Table aria-label="Invoices">
<TableHeader>
<TableRow className="hover:bg-transparent">
<TableHead>Invoice</TableHead>
<TableHead>Customer</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{INVOICES.map((invoice) => (
<TableRow key={invoice.number}>
<TableCell className="font-mono text-[13px]">{invoice.number}</TableCell>
<TableCell>{invoice.customer}</TableCell>
<TableCell>
<StatusBadge {...INVOICE_STATUS[invoice.status]} />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
}StatusBadge for the state of a record. For a static tag, a plan, a version or a count, use Badge.StatusBadge has a label. A StatusDot with no text next to it needs label.pulse or spin only while the work runs.Import the components. Each component has one part.
import { StatusBadge, StatusDot, StatusLine } from 'ferry-ui'
<StatusBadge tone="success" label="Active" />
<StatusDot tone="success" />
<StatusLine tone="success">Project is active</StatusLine>| Component | Look | Place |
|---|---|---|
StatusBadge | An uppercase pill with a dot. | Tables, headers, tiles and cards. |
StatusDot | A 6px dot. | Before a name in a list or a menu. |
StatusLine | An icon in a circle, then a sentence. | Card footers and summary panels. |
The three components use the same tone prop. STATUS_TONES lists the tones in display order.
import { STATUS_TONES, StatusBadge, type StatusTone } from 'ferry-ui'
const LABELS: Record<StatusTone, string> = {
success: 'Active',
warning: 'Past due',
destructive: 'Failed',
info: 'In review',
neutral: 'Draft',
}
export default function StatusTones() {
return (
<>
{STATUS_TONES.map((tone) => (
<StatusBadge key={tone} tone={tone} label={LABELS[tone]} />
))}
</>
)
}| Tone | Meaning |
|---|---|
success | Healthy, done, active, paid. |
warning | Degraded, needs attention, expiring. |
destructive | Failed, overdue, blocked. |
info | In progress, pending review. |
neutral (default) | Inactive, paused, draft, unknown. |
A StatusBadge is 20px high. Next to a page title or a card title, use size="sm". It is 18px high.
import { StatusBadge } from 'ferry-ui'
export default function StatusSizes() {
return (
<div className="flex flex-col gap-4">
<StatusBadge tone="success" label="Active" />
<div className="flex items-center gap-2">
<span className="text-base font-medium text-foreground">Billing portal</span>
<StatusBadge tone="success" label="Active" size="sm" />
</div>
</div>
)
}Set pulse to add a pulse to the dot. Remove it when the work stops.
import * as React from 'react'
import { Button, StatusBadge } from 'ferry-ui'
export default function StatusPulse() {
const [running, setRunning] = React.useState(true)
return (
<>
{running ? (
<StatusBadge tone="info" label="Running" pulse />
) : (
<StatusBadge tone="neutral" label="Stopped" />
)}
<Button size="tiny" onClick={() => setRunning((value) => !value)}>
{running ? 'Stop the import' : 'Start the import'}
</Button>
</>
)
}An icon replaces the dot. To show the icon and the dot, set dot. To hide the dot, set dot to false.
import { StatusBadge } from 'ferry-ui'
import { CreditCard, Lock, ShieldAlert } from 'lucide-react'
export default function StatusIcon() {
return (
<>
<StatusBadge tone="destructive" icon={<ShieldAlert />} label="Blocked" />
<StatusBadge tone="neutral" icon={<Lock />} label="Private" />
<StatusBadge tone="warning" icon={<CreditCard />} label="Card expires" dot />
<StatusBadge tone="neutral" label="Draft" dot={false} />
</>
)
}Put a StatusDot before text that names the state. If no text names the state, give the dot a label.
import { StatusDot } from 'ferry-ui'
export default function StatusDots() {
return (
<ul className="flex flex-col gap-2 text-[13px] text-foreground-light">
<li className="flex items-center gap-2">
<StatusDot tone="success" /> 3 endpoints online
</li>
<li className="flex items-center gap-2">
<StatusDot tone="info" pulse /> Live updates on
</li>
<li className="flex items-center gap-2">
{/* No text names the state: the dot needs a label. */}
<StatusDot tone="destructive" label="Offline" />
<span className="font-mono">orders-webhook</span>
</li>
</ul>
)
}A StatusLine shows an icon for its tone, then one line of text. With spin, the icon is a spinner. Use icon to give another icon.
import { StatusLine } from 'ferry-ui'
import { Pause } from 'lucide-react'
export default function StatusLines() {
return (
<div className="flex flex-col gap-3">
<StatusLine tone="success">Workspace is active</StatusLine>
<StatusLine tone="warning">Usage is above 90% of the plan</StatusLine>
<StatusLine tone="destructive">Payment failed</StatusLine>
<StatusLine tone="info" spin>
Import of contacts in progress
</StatusLine>
<StatusLine tone="neutral" icon={<Pause />}>
Subscription is on hold
</StatusLine>
</div>
)
}statusBadgeVariants returns the classes of the badge. Use it to give the badge look to a button or a link.
import * as React from 'react'
import { StatusDot, cn, statusBadgeVariants, type StatusTone } from 'ferry-ui'
const FILTERS: { tone: StatusTone; label: string }[] = [
{ tone: 'success', label: 'Paid' },
{ tone: 'warning', label: 'Overdue' },
{ tone: 'destructive', label: 'Failed' },
]
export default function StatusVariants() {
const [active, setActive] = React.useState<StatusTone | null>('success')
return (
<div role="group" aria-label="Filter invoices by status" className="flex flex-wrap items-center gap-2">
{FILTERS.map(({ tone, label }) => {
const pressed = active === tone
return (
<button
key={tone}
type="button"
aria-pressed={pressed}
onClick={() => setActive(pressed ? null : tone)}
className={cn(
statusBadgeVariants({ tone: pressed ? tone : 'neutral' }),
'cursor-pointer outline-none focus-visible:ring-2 focus-visible:ring-ring',
)}
>
<StatusDot tone={tone} />
{label}
</button>
)
})}
</div>
)
}StatusDot with no label is decorative. With a label, it is an image with this name.StatusLine is decorative. Write the state in the text.The three components also accept each attribute of the <span> element.
| Prop | Type | Default |
|---|---|---|
labelRequired | ReactNode | - |
The state, 1–3 words ("Active", "Past due", "Syncing"). Rendered uppercase; it never wraps. | ||
tone | "destructive" | "warning" | "success" | "info" | "neutral" | neutral |
Colour of the badge (and of its dot). Defaults to | ||
size | "sm" | "md" | md |
Height and padding: | ||
pulse | boolean | false |
Pulses the leading dot for live or in-progress states ("Running", "Processing"). No effect when the dot is hidden ( | ||
icon | ReactNode | - |
Leading icon (lucide, sized to 12px). When set, the dot is hidden unless | ||
dot | boolean | - |
Shows the leading status dot. Defaults to | ||
| Prop | Type | Default |
|---|---|---|
tone | "destructive" | "warning" | "success" | "info" | "neutral" | neutral |
Colour of the dot. Defaults to | ||
pulse | boolean | false |
Adds a soft ping ring around the dot. Use it only for live or in-progress states (running, syncing, recording). | ||
label | string | - |
Accessible name ("Online", "Syncing"). Without it the dot is decorative ( | ||
| Prop | Type | Default |
|---|---|---|
childrenRequired | ReactNode | - |
The sentence describing the state ("Workspace is active", "Payment failed"). Truncates on one line. | ||
tone | "destructive" | "warning" | "success" | "info" | "neutral" | neutral |
Colour of the circled icon. Defaults to | ||
icon | ReactNode | - |
Icon inside the 20px circle (sized to 12px). Defaults to a per-tone icon: check (success), triangle (warning), cross (destructive), info (info), dashed circle (neutral) — or a spinner when | ||
spin | boolean | false |
Spins the icon for in-progress states ("Syncing…", "Importing…"). Without | ||
iconClassName | string | - |
Extra classes for the circle, e.g. a custom ring colour ( | ||
| Name | Type | Role |
|---|---|---|
statusBadgeVariants | ({ tone, size }) => string | Returns the classes of a StatusBadge. |
STATUS_TONES | readonly StatusTone[] | Lists the five tones in display order. |
StatusTone | type | 'success', 'warning', 'destructive', 'info' or 'neutral'. |