Status Badge
A payment status pill built on the shadcn Badge. Each state maps onto a shared colour ramp — success, warning, danger, info — so the same meaning renders the same way everywhere in the product.
import { PAYMENT_STATUSES, StatusBadge } from '@/components/custom/status-badge'
export default function StatusBadgeDemo() {
return (
<div className="flex flex-wrap items-center justify-center gap-2">
{PAYMENT_STATUSES.map((status) => (
<StatusBadge key={status} status={status} />
))}
</div>
)
}Installation
pnpm dlx shadcn@latest add https://kiotapay-ui-staging.staging.kiotapay.com/r/status-badge.jsonnpx shadcn@latest add https://kiotapay-ui-staging.staging.kiotapay.com/r/status-badge.jsonyarn dlx shadcn@latest add https://kiotapay-ui-staging.staging.kiotapay.com/r/status-badge.jsonbunx --bun shadcn@latest add https://kiotapay-ui-staging.staging.kiotapay.com/r/status-badge.jsonUsage
import { StatusBadge } from '@/components/custom/status-badge'
export function PaymentRow() {
return <StatusBadge status="success" />
}Props
Extends React.ComponentProps<typeof Badge> minus variant and children.
| Prop | Type | Default | Description |
|---|---|---|---|
status | PaymentStatus | — | One of success, pending, failed, refunded, neutral. |
label | ReactNode | from STATUS_LABELS | Replaces the default wording. |
withDot | boolean | true | Show the leading dot. Turn off in dense tables. |
size | 'sm' | 'md' | 'md' | Pill size. |
Exports
| Export | Description |
|---|---|
StatusBadge | The component. |
PaymentStatus | Union type of the five statuses. |
PAYMENT_STATUSES | Readonly array of every status, for iteration. |
STATUS_LABELS | Default label per status. |
statusBadgeVariants | The underlying cva function, for composing elsewhere. |
Examples
In a table
Dense rows read better at size="sm" with the dot suppressed — the column position already establishes what the value means.
import { StatusBadge, type PaymentStatus } from '@/components/custom/status-badge'
import { formatMoney } from '@/lib/kiota-format'
const PAYMENTS: ReadonlyArray<{ reference: string; amount: number; status: PaymentStatus }> = [
{ reference: 'KP-9F2A41', amount: 125050, status: 'success' },
{ reference: 'KP-7C1B08', amount: 48000, status: 'pending' },
{ reference: 'KP-2D9E33', amount: 990000, status: 'failed' },
{ reference: 'KP-5A0C77', amount: 32500, status: 'refunded' },
]
export default function StatusBadgeTable() {
return (
<div className="w-full overflow-x-auto">
<table className="w-full min-w-[22rem] text-sm">
<thead>
<tr className="border-b text-left text-muted-foreground">
<th className="pb-2 font-medium">Reference</th>
<th className="pb-2 text-right font-medium">Amount</th>
<th className="pb-2 text-right font-medium">Status</th>
</tr>
</thead>
<tbody>
{PAYMENTS.map((payment) => (
<tr key={payment.reference} className="border-b last:border-0">
<td className="py-2.5 font-mono text-xs">{payment.reference}</td>
<td className="py-2.5 text-right tabular-nums">{formatMoney(payment.amount)}</td>
<td className="py-2.5 text-right">
{/* Dense rows: drop the dot and step down a size. */}
<StatusBadge status={payment.status} size="sm" withDot={false} />
</td>
</tr>
))}
</tbody>
</table>
</div>
)
}Custom labels
The status drives colour; the label is free text. Use this when a surface has its own vocabulary — a refund queue calling success "Settled", say.
<StatusBadge status="success" label="Settled" />
<StatusBadge status="pending" label="Awaiting M-Pesa" />
<StatusBadge status="neutral" label="Not submitted" />Accessibility
Colour alone fails WCAG 1.4.1, so the label always ships alongside the pill and the dot only reinforces it. Removing the text and keeping the dot would leave the state unreadable to anyone who cannot distinguish the hues — if you need a compact form, use size="sm" rather than dropping the label.
Theming
Payment states do not own colours. They map onto the shared state ramps installed by kiota-theme, so one hue means one thing everywhere in the product:
| Status | Ramp | Renders as |
|---|---|---|
success | success | bg-success-light text-success |
pending | warning | bg-warning-light text-warning |
failed | danger | bg-danger-light text-danger |
refunded | info | bg-info-light text-info |
neutral | shadcn muted | bg-muted text-muted-foreground |
Each ramp is three tokens in both themes — the solid colour, a readable -foreground for text on the solid, and a -light tint used as a surface. Recolour a state by redefining them after the import that declares them:
:root {
--success: oklch(0.52 0.13 155);
--success-foreground: oklch(0.985 0 0);
--success-light: oklch(0.95 0.05 155);
}
.dark {
--success: oklch(0.8 0.14 155);
--success-foreground: oklch(0.2 0.04 155);
--success-light: oklch(0.28 0.06 155);
}Because these are real Tailwind theme colours, the utilities work anywhere in your app — bg-success, text-danger, border-warning/25 — not just inside this component.
Keep roughly 0.4 OKLCH lightness between a ramp's solid and its -light tint. The component pairs them as text on
surface, and a closer pair drops below 4.5:1 at badge sizes even though it still looks like two distinct colours.
Badges rendering unstyled means the tokens never reached your stylesheet. Re-run the add command. Note that cssVars
are applied at install time and re-runs do not reliably update existing variables — see Updating.