Skip to content
Highlight

V2 componentsUpdated Oct 14, 2026

Badge

A small label for metadata: durations, counts, sources, status, tags. Static by default, and it must look static: the default subtle variant is a borderless wash, because an edge on a control surface is what reads as "press me". Only a badge that does something takes variant="outline" and a render element (render={<button type="button" onClick={…} />} or render={<a href="…" />}), which adds hover, press and a focus ring. Use tone for meaning (status tones) or grouping (category tones), never a className tint. For a dropdown on a badge, use Select with size="badge".

Source: packages/ui/src/components/badge

tsx
import { Badge } from '@highlight/ui'

Change the props; the preview and the snippet update together.

45 min
Props
Variant
Tone
Size
Shape
Left
Right
Dot
Interactive (render button)
On clear
tsx
<Badge left={<Clock />}>45 min</Badge>

Icons and images in the left slot are sized for you. dot adds a status mark in the tone.

DefaultTomorrow, 9:00BrandDoneBlocked
tsx
import { Calendar } from 'lucide-react'import { Badge } from '@highlight/ui' export default function BadgeBasic() {  return (    <div className="flex flex-wrap items-center justify-center gap-2">      <Badge>Default</Badge>      <Badge left={<Calendar />}>Tomorrow, 9:00</Badge>      <Badge tone="brand">Brand</Badge>      <Badge tone="success" dot>        Done      </Badge>      <Badge tone="danger" dot>        Blocked      </Badge>    </div>  )}

A static badge has no edge and no hover. outline + render={<button />} reads as a control. onClear adds a remove button (static badges only).

Static: start of document
LinJoshVaun
tsx
import { useState } from 'react'import { Badge } from '@highlight/ui'import { ChevronDown, FileText } from 'lucide-react' export default function BadgeInteractive() {  const [people, setPeople] = useState(['Lin', 'Josh', 'Vaun'])   return (    <div className="flex flex-col items-center gap-4">      <div className="flex flex-wrap items-center gap-2">        <Badge left={<FileText />}>Static: start of document</Badge>        <Badge variant="outline" left={<FileText />} right={<ChevronDown />} render={<button type="button" />}>          Pressable: start of document        </Badge>      </div>      <div className="flex flex-wrap items-center gap-2">        {people.map((name) => (          <Badge            key={name}            onClear={() => setPeople((all) => all.filter((n) => n !== name))}            clearLabel={`Remove ${name}`}          >            {name}          </Badge>        ))}      </div>    </div>  )}

subtle for most labels; solid only for a count or status that must be seen at a glance.

neutralbrandinfosuccesswarningdanger
neutralbrandinfosuccesswarningdanger
tsx
import { BADGE_TONES, Badge } from '@highlight/ui' // The status tones: everything before the categories.const TONES = BADGE_TONES.slice(0, 6) export default function BadgeVariants() {  return (    <div className="grid gap-2">      {(['subtle', 'solid'] as const).map((variant) => (        <div key={variant} className="flex flex-wrap items-center gap-2">          {TONES.map((tone) => (            <Badge key={tone} variant={variant} tone={tone} className="capitalize">              {tone}            </Badge>          ))}        </div>      ))}    </div>  )}

No meaning, only grouping: labels, projects, people.

yellowtealindigopurplepinkbrown
tsx
import { BADGE_TONES, Badge } from '@highlight/ui' const CATEGORIES = BADGE_TONES.slice(6) export default function BadgeCategories() {  return (    <div className="flex flex-wrap items-center justify-center gap-2">      {CATEGORIES.map((tone) => (        <Badge key={tone} tone={tone} className="capitalize">          {tone}        </Badge>      ))}    </div>  )}

16 to 32px. md (24) is the default.

xssmmdlgxl
tsx
import { Badge } from '@highlight/ui'import { Clock } from 'lucide-react' const SIZES = ['xs', 'sm', 'md', 'lg', 'xl'] as const export default function BadgeSizes() {  return (    <div className="flex flex-wrap items-center justify-center gap-2">      {SIZES.map((size) => (        <Badge key={size} size={size} left={<Clock />}>          {size}        </Badge>      ))}    </div>  )}

An img child is rounded and sized like an icon.

LinVaun
tsx
import { Badge } from '@highlight/ui' export default function BadgeAvatar() {  return (    <div className="flex flex-wrap items-center justify-center gap-2">      <Badge>        <img src="https://api.dicebear.com/9.x/initials/svg?seed=Lin" alt="" />        Lin      </Badge>      <Badge>        <img src="https://api.dicebear.com/9.x/initials/svg?seed=Vaun" alt="" />        Vaun      </Badge>    </div>  )}

The root, then any compound sub-components. Every part accepts className and forwards the rest of its props to the element or Base UI part beneath it.

Badge
The pill: a span, or a button / a when given onClick / href. left and right take an icon or image beside the label.

Props added on top of the underlying element or Base UI part.

PropTypeDefaultDescription
variant'subtle' | 'outline' | 'solid'—subtle (default) is static; outline is for a badge that is pressed; solid for loud counts.
toneBadgeTone (BADGE_TONES)—Status tones carry meaning; the category tones only tell groups apart.
size'xs' | 'sm' | 'md' | 'lg' | 'xl'—16, 20, 24, 28, 32px tall.
shape'pill' | 'squircle'—pill (default) or a squircle at half the height.
dotboolean—A status dot before the label.
left / rightReactNode—Leading and trailing slots.
onClick / hrefhandler / string—Renders a <button> / <a> and turns on interactive states.
onClear / clearLabelhandler / string—Trailing remove button and its accessible name.