Status badge
A Badge whose color and text come from a status through a map. Declare the map once, next to the data, and every screen renders the status the same way.
import { StatusBadge, type StatusMap } from '@/components/status-badge'
type PaymentStatus = 'succeeded' | 'processing' | 'pending' | 'failed' | 'cancelled' | 'expired'
const paymentStatus: StatusMap<PaymentStatus> = {
succeeded: 'green',
processing: 'sky',
pending: 'amber',
failed: 'red',
cancelled: 'zinc',
expired: 'zinc',
}
export default function Example({ status }: { status: PaymentStatus }) {
return <StatusBadge status={status} map={paymentStatus} />
}<script setup lang="ts">
import StatusBadge from '@/components/status-badge.vue'
defineProps<{ status: string }>()
const paymentStatus = {
succeeded: 'green',
processing: 'sky',
pending: 'amber',
failed: 'red',
cancelled: 'zinc',
expired: 'zinc',
} as const
</script>
<template>
<StatusBadge :status="status" :map="paymentStatus" />
</template>Installation
Copy these files into the folder where you keep your components. They import each other with relative paths, so the folder can live anywhere. The packages they rely on are listed on the Installation page.
import type React from 'react'
import { Badge } from './badge'
type BadgeColor = NonNullable<React.ComponentProps<typeof Badge>['color']>
type Entry = BadgeColor | { color: BadgeColor; label?: React.ReactNode }
export type StatusMap<S extends string> = Record<S, Entry>
export function StatusBadge<S extends string>({
status,
map,
fallback = 'zinc',
...props
}: {
status: S | (string & {})
map: StatusMap<S>
fallback?: BadgeColor
} & Omit<React.ComponentPropsWithoutRef<typeof Badge>, 'color' | 'children'>) {
let entry = (map as Record<string, Entry | undefined>)[status]
let color = entry === undefined ? fallback : typeof entry === 'string' ? entry : entry.color
let label = entry !== undefined && typeof entry !== 'string' && entry.label !== undefined ? entry.label : status
return (
<Badge {...props} color={color}>
{label}
</Badge>
)
}Also copy Badge.
<script setup lang="ts" generic="S extends string">
import { computed } from 'vue'
import Badge, { type BadgeColor } from './badge.vue'
const props = withDefaults(
defineProps<{
status: S | (string & {})
map: Record<S, BadgeColor | { color: BadgeColor; label?: string }>
fallback?: BadgeColor
}>(),
{ fallback: 'zinc' }
)
const entry = computed(() => (props.map as Record<string, BadgeColor | { color: BadgeColor; label?: string } | undefined>)[props.status])
const color = computed(() =>
entry.value === undefined ? props.fallback : typeof entry.value === 'string' ? entry.value : entry.value.color
)
const label = computed(() =>
entry.value !== undefined && typeof entry.value !== 'string' && entry.value.label !== undefined
? entry.value.label
: props.status
)
</script>
<template>
<Badge :color="color">{{ label }}</Badge>
</template>Also copy Badge.
Component API
| Prop | Default | Description |
|---|---|---|
StatusBadgerenders a <Badge> element | ||
status | - | The value to render, looked up in the map. |
map | - | Color of each status, or `{ color, label }` when the text shown differs from the key. |
fallback | zinc | Color used for a status the map does not know. |
Examples
Basic example
Map each status to a Badge color. The key is rendered as the text, so add className="capitalize" when the keys are lowercase:
import { StatusBadge, type StatusMap } from '@/components/status-badge'
type PaymentStatus = 'succeeded' | 'processing' | 'pending' | 'failed' | 'cancelled' | 'expired'
const paymentStatus: StatusMap<PaymentStatus> = {
succeeded: 'green',
processing: 'sky',
pending: 'amber',
failed: 'red',
cancelled: 'zinc',
expired: 'zinc',
}
export default function Example({ status }: { status: PaymentStatus }) {
return <StatusBadge status={status} map={paymentStatus} />
}<script setup lang="ts">
import StatusBadge from '@/components/status-badge.vue'
defineProps<{ status: string }>()
const paymentStatus = {
succeeded: 'green',
processing: 'sky',
pending: 'amber',
failed: 'red',
cancelled: 'zinc',
expired: 'zinc',
} as const
</script>
<template>
<StatusBadge :status="status" :map="paymentStatus" />
</template>With labels
When the text differs from the key, give the entry a label next to its color:
const disputeStatus: StatusMap<DisputeStatus> = {
open: { color: 'amber', label: 'Open' },
under_review: { color: 'blue', label: 'Under review' },
won: { color: 'green', label: 'Won' },
lost: { color: 'red', label: 'Lost' },
resolved: { color: 'zinc', label: 'Resolved' },
}
<StatusBadge status="under_review" map={disputeStatus} />const disputeStatus = {
open: { color: 'amber', label: 'Open' },
under_review: { color: 'blue', label: 'Under review' },
won: { color: 'green', label: 'Won' },
lost: { color: 'red', label: 'Lost' },
resolved: { color: 'zinc', label: 'Resolved' },
} as const
<StatusBadge status="under_review" :map="disputeStatus" />Unknown status
A status missing from the map still renders, in the fallback color, so a new value from the API never shows up unstyled:
const status: string = 'archived'
<StatusBadge status={status} map={paymentStatus} />
<StatusBadge status={status} map={paymentStatus} fallback="amber" /><StatusBadge :status="payment.status" :map="paymentStatus" />
<StatusBadge :status="payment.status" :map="paymentStatus" fallback="amber" />In a table
The usual place: a status column that reads the row and the shared map:
| Payment | Customer | Amount | Status |
|---|---|---|---|
| pay_8f2a | Awa Diallo | €125.00 | succeeded |
| pay_9c41 | Moussa Traoré | €48.00 | processing |
| pay_a07d | Nadia Market | €310.00 | failed |
<TableCell>
<StatusBadge status={payment.status} map={paymentStatus} />
</TableCell><TableCell>
<StatusBadge :status="payment.status" :map="paymentStatus" />
</TableCell>