Badge
The one rounded shape: a hairline pill for a tag or a status; or tags written as one list in the italic; or a seal with its legend set round a ring.
Report an issue with BadgeExample
DraftNewLive
Showing work in .
RestDraft
InkNew
AccentLive
Removable
Pointed at
SeriesIdentityWebMotion
Series, pointed at
Seal
Seal, pointed at
Install
Run it in a project that has the shadcn CLI set up. It brings the 0nlyType base along.
npx shadcn@latest add https:// thedirectors.agency/ ui/ r/badge.jsonUsage
This example also uses Button.
npx shadcn@latest add https:// thedirectors.agency/ ui/ r/ button.json"use client"import * as React from "react"import { Badge } from "@/components/ui/badge"import { Button } from "@/components/ui/button"const all = ["Identity", "Web", "Motion", "Print"]function Series() {const [tags, setTags] = React.useState(all)return (<div className="flex flex-col items-start" style={{ gap: "var(--ot-space-4)" }}><p className="ot-mp" style={{ margin: 0, maxWidth: "30ch" }}>{tags.length ? "Showing work in " : "Showing all the work."}{tags.map((t) => (<Badge key={t} variant="series" onRemove={() => setTags((c) => c.filter((x) => x !== t))}>{t}</Badge>))}{tags.length ? "." : null}</p>{tags.length === 0 ? <Button variant="quiet" onClick={() => setTags(all)}>Put them back</Button> : null}</div>)}export default function Example() {const [tags, setTags] = React.useState(all)return (<div className="flex flex-col items-start" style={{ gap: "var(--ot-space-6)" }}><div className="flex flex-col items-start" style={{ gap: "var(--ot-space-5)" }}><div className="flex flex-wrap gap-3">{tags.map((t) => (<Badge key={t} onRemove={() => setTags((c) => c.filter((x) => x !== t))}>{t}</Badge>))}</div>{tags.length === 0 ? (<Button variant="quiet" onClick={() => setTags(all)}>Put them back</Button>) : null}<div className="flex flex-wrap gap-3"><Badge>Draft</Badge><Badge variant="ink">New</Badge><Badge variant="accent">Live</Badge></div></div><Series /><div className="flex flex-wrap items-center" style={{ gap: "var(--ot-space-6)" }}><Badge variant="seal" legend="First edition · 2026">01</Badge><Badge variant="seal" legend="Set in Parma · Bodoni">B</Badge></div></div>)}Contract
- Underneath: native.
- Anatomy:
<span class="ot-tag" data-variant="ink | accent">, or a removable<button class="ot-tag" data-removable aria-label="Remove X">. - The label sits in a
<span>. Pointing at or focusing a removable tag strikes the word, as the checkbox does. - The one rounded shape. A removed tag, already struck, closes up (its width goes to 0) while its neighbours slide into the space, and focus moves to the next.
- The word is set as Weingart set his callouts: small, a little heavy and narrow (500, width 92%, in ink), so it holds as type inside its hairline; the ink pill knocks it out heavier and narrower still (650, 88%). Forced colours draw the pill as a CanvasText outline and the strike as a line-through.
- series: the written list. No pill: sibling tags are one sentence ("Showing work in Identity, Web and Motion."), the tags in the expression italic because they are yours, the commas and the "and" ours, in the voice and graphite. The punctuation is worked out in CSS from each tag's place among its siblings (
:nth-last-child(… of …)), and a leaving tag stops counting at once, so as a tag closes up the list re-sets itself around the gap ("Identity and Motion").--andon the tags changes the conjunction. Removable series tags keep the strike and the close-up. - seal: a coin.
legendis set round the rim between two hairline rings (SVGtextPath,textLengthspread to the whole circle so the letters stand evenly, closing on a dot), the face in the middle atmf250. It's space-8 across and named as an image ("01, First edition · 2026"). Pointing turns the legend 30° (andante, breath), as a coin turns in the fingers; it turns back when you leave.
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
variant | "default" | "ink" | "accent" | "series" | "seal" | "default" | Ink is reversed type in a filled pill, for one new thing. Accent is the view's one accent, for a live state. series drops the pill: sibling tags are written as one list in the italic (yours), and ours are the commas and the "and", which re-set themselves as a tag leaves; set --and on the tags to change the conjunction. seal is a coin: the legend set round a hairline ring, the children (a figure or up to three letters) in the middle; pointing turns the legend a little. |
legend | string | seal only: the words set round the rim, about 12 to 24 characters. The seal is an image named by its face and its legend ("01, First edition · 2026"); pass aria-label if the face isn't plain text. | |
onRemove | () => void | Makes the tag removable: the whole pill becomes a button with a cross, named Remove followed by the tag. Pointing at it strikes the word. It closes up, focus moves to the neighbouring tag, then onRemove runs. Pass aria-label if the children aren't plain text. Works with series; not with seal. |
Craft
Where its move comes from
| Component | Reference | Move |
|---|---|---|
| Tag | The checkbox; Weingart callouts | Removing strikes the word |
| Series tags | The written list (serial punctuation); "Healthy habits → for creatives" | Tags written as one sentence; the commas and "and" re-set as one leaves |
| Seal | A coin's legend; WOVE's numbers on an arc | The legend set round a ring, the face in the middle |
How it moves
| Component | Articulation | What moves |
|---|---|---|
| Tag | Close-up | Struck, then closed; neighbours slide in |
| Series tags | Close-up | As the tag, and the punctuation re-sets round the gap |
| Seal | Turn | Pointing turns the legend 30°, andante; leaving turns it back |