VIII Wayfinding

Star on GitHubFeedback
VIII WayfindingNative HTMLot-tag

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 Badge

Example

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.json

Usage

This example also uses Button.

npx shadcn@latest add https://thedirectors.agency/ui/r/button.json
badge-example.tsx
"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"). --and on the tags changes the conjunction. Removable series tags keep the strike and the close-up.
  • seal: a coin. legend is set round the rim between two hairline rings (SVG textPath, textLength spread to the whole circle so the letters stand evenly, closing on a dot), the face in the middle at mf 250. 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

Props of Badge
PropTypeDefaultWhat 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.
legendstringseal 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() => voidMakes 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

Where its move comes from
ComponentReferenceMove
TagThe checkbox; Weingart calloutsRemoving strikes the word
Series tagsThe written list (serial punctuation); "Healthy habits → for creatives"Tags written as one sentence; the commas and "and" re-set as one leaves
SealA coin's legend; WOVE's numbers on an arcThe legend set round a ring, the face in the middle

How it moves

How it moves
ComponentArticulationWhat moves
TagClose-upStruck, then closed; neighbours slide in
Series tagsClose-upAs the tag, and the punctuation re-sets round the gap
SealTurnPointing turns the legend 30°, andante; leaving turns it back