Skip to content
  1. Records
  2. Status badge

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.

succeededprocessingpendingfailedcancelledexpired
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

PropDefaultDescription
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.
fallbackzincColor 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:

succeededprocessingpendingfailedcancelledexpired
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:

OpenUnder reviewWonLostResolved
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:

archivedarchived
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:

PaymentCustomerAmountStatus
pay_8f2aAwa Diallo€125.00succeeded
pay_9c41Moussa Traoré€48.00processing
pay_a07dNadia Market€310.00failed
<TableCell>
  <StatusBadge status={payment.status} map={paymentStatus} />
</TableCell>
<TableCell>
  <StatusBadge :status="payment.status" :map="paymentStatus" />
</TableCell>