Skip to contentVibraUI
Data display

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-badge

Needs the @vibra registry in your components.json — set it up once.

Examples

Props

PropTypeDefaultDescription
priority"urgent" | "high" | "medium" | "low" | "none"—The level; it sets both the icon and the tone.
showLabelbooleantrueFalse 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.
classNamestring—Merged onto the span root; the remaining span props are spread onto it too.
PRIORITY_LABELSRecord<Priority, string>—The word each priority reads as — Urgent, High, Medium, Low, None.

Dependencies

Source

components/ui/priority-badge.tsx
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 }