Priority badge
A ticket's priority as a muted pill with its own icon, from urgent down to none.
Server-compatible: no hooks, no client boundary. Each priority has its own icon as well as its own tone — a triangle for urgent, a double chevron for high, an equals sign for medium, a chevron down for low, a dash for none — so the level reads at a glance without relying on colour. showLabel={false} hides the word on screen but keeps it for a screen reader, which is what makes an icon-only badge safe in a narrow column.
Install
$
npx shadcn@latest add @vibra/priority-badgeNeeds the @vibra registry in your components.json — set it up once.
Examples
import { DataList, DataListItem } from "@/components/ui/data-list"
import { PriorityBadge, type Priority } from "@/components/ui/priority-badge"
type Ticket = { id: string; title: string; assignee: string; priority: Priority }
const tickets: Ticket[] = [
{ id: "VIB-482", title: "Checkout fails on Safari 17", assignee: "Olivia Martin", priority: "urgent" },
{ id: "VIB-479", title: "Add SSO to the admin console", assignee: "Jackson Lee", priority: "high" },
{ id: "VIB-471", title: "Tune the search index refresh", assignee: "Isabella Nguyen", priority: "medium" },
{ id: "VIB-465", title: "Rewrite the onboarding copy", assignee: "William Kim", priority: "low" },
{ id: "VIB-460", title: "Archive the 2024 export job", assignee: "Sofia Davis", priority: "none" },
]
export default function PriorityBadgeDemo() {
return (
<div className="w-full max-w-lg overflow-hidden rounded-lg border">
<DataList divided>
{tickets.map((ticket) => (
<DataListItem
key={ticket.id}
title={ticket.title}
description={
<span>
<span className="font-mono">{ticket.id}</span>
<span> · {ticket.assignee}</span>
</span>
}
actions={<PriorityBadge priority={ticket.priority} />}
/>
))}
</DataList>
</div>
)
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| priority | "urgent" | "high" | "medium" | "low" | "none" | — | The level; it sets both the icon and the tone. |
| showLabel | boolean | true | False leaves the icon alone on screen; the word still reaches a screen reader. |
| size | "sm" | "default" | "default" | sm tightens the padding, drops the text to 11px, and shrinks the icon to size-3. |
| className | string | — | Merged onto the span root; the remaining span props are spread onto it too. |
| PRIORITY_LABELS | Record<Priority, string> | — | The word each priority reads as — Urgent, High, Medium, Low, None. |
Dependencies
Source
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import {
ChevronDownIcon,
ChevronsUpIcon,
EqualIcon,
MinusIcon,
TriangleAlertIcon,
} from "lucide-react"
import { cn } from "@/lib/utils"
const priorityBadgeVariants = cva(
"inline-flex w-fit items-center rounded-full font-medium whitespace-nowrap [&_svg]:pointer-events-none [&_svg]:shrink-0",
{
variants: {
priority: {
urgent: "bg-danger-muted text-danger",
high: "bg-warning-muted text-warning",
medium: "bg-info-muted text-info",
low: "bg-muted text-muted-foreground",
none: "bg-muted text-muted-foreground",
},
size: {
default: "gap-1 px-2 py-0.5 text-xs [&_svg:not([class*='size-'])]:size-3.5",
sm: "gap-1 px-1.5 py-0 text-2xs [&_svg:not([class*='size-'])]:size-3",
},
},
defaultVariants: { priority: "none", size: "default" },
}
)
// Derived from the pill's own variants, so a level can never have a label and
// an icon without a class to paint it.
export type Priority = NonNullable<VariantProps<typeof priorityBadgeVariants>["priority"]>
/** The word each priority reads as; it is always rendered, visibly or for a screen reader. */
export const PRIORITY_LABELS: Record<Priority, string> = {
urgent: "Urgent",
high: "High",
medium: "Medium",
low: "Low",
none: "None",
}
// Shape as well as colour: the arrow says which way the priority points even
// where the tone does not read.
const PRIORITY_ICONS: Record<Priority, React.ComponentType<{ className?: string }>> = {
urgent: TriangleAlertIcon,
high: ChevronsUpIcon,
medium: EqualIcon,
low: ChevronDownIcon,
none: MinusIcon,
}
export type PriorityBadgeProps = React.ComponentProps<"span"> & {
priority: Priority
/** False leaves the icon alone on screen; the word still reaches a screen reader. */
showLabel?: boolean
size?: NonNullable<VariantProps<typeof priorityBadgeVariants>["size"]>
}
/** A ticket's priority as a muted pill with its own icon, so it never reads by colour alone. */
function PriorityBadge({
className,
priority,
showLabel = true,
size = "default",
...props
}: PriorityBadgeProps) {
const Icon = PRIORITY_ICONS[priority]
return (
<span
data-slot="priority-badge"
data-priority={priority}
data-size={size}
className={cn(
priorityBadgeVariants({ priority, size }),
!showLabel && (size === "sm" ? "px-1" : "px-1.5"),
className
)}
{...props}
>
<Icon data-slot="priority-badge-icon" aria-hidden="true" />
<span className={cn(!showLabel && "sr-only")}>{PRIORITY_LABELS[priority]}</span>
</span>
)
}
export { PriorityBadge, priorityBadgeVariants }