Display
Tag
A subject label — a topic, a technology, a filter facet — that filters with onClick and is dismissed with onRemove.
Usage
When to reach for it
import { Tag } from '@misoto22/design'Notes
A subject label — a topic, a technology, a filter facet — that can filter with onClick and be dismissed with onRemove.
Distinct from Badge, which carries a state or a count. A tag names what something is ABOUT, so several sit together in a row and the reader scans them; a badge is one fact about one record.
This is where a Token component would have gone. It was not built: a token is a tag with a remove button, and the difference between the two is one prop, not one component. The system already ships three things that look alike — Badge, Tag, StatusPill — and a fourth whose whole distinction is an X on the end would be the one a call site picks by coin toss.
Presentational until it is given a handler, and the component owns both interactive cases rather than leaving one to a wrapper at the call site. That is not a convenience: a wrapping <button> around a chip that already holds the remove <button> is a button inside a button, which no parser keeps and no accessibility tree reports the way it was written. Given both, the chip renders the label and the X as SIBLING buttons — the label takes the leading padding with it, so it is the whole of the chip up to the X rather than the words with dead padding around them.
Anatomy
| Element | Description |
|---|---|
| Chiprequired | The <span>: the same --radius-sm corner, the same 10px by 4px padding and the same 12px mono as a Badge, with no border of its own. |
| Groundrequired | --stone at rest, --accent once active, cross-fading over --duration-fast. The accent is the system’s one pointer at a choice, which is why it is what selection is drawn in. |
| Labelrequired | children, in --ink-3-aa — the AA floor rather than a light grey — and in --accent-foreground once active. |
| Remove button | On onRemove only: a real <button type="button"> after the label, holding a 12px X and named by removeLabel. The drawn box is 16px, under the 24px WCAG 2.5.8 floor, so an inset pseudo-element takes the hit area out to 24 without changing the drawing or pushing the chips apart. |
| Filter control | On onClick only: the chip itself becomes the <button>, carrying aria-pressed from active. Given onRemove as well, the label splits into its own button beside the X — two siblings, never one inside the other — and takes the leading padding with it so the target is the chip up to the X rather than just the words. |
Best practices
Do
- Filter with onClick, not with a wrapper. The chip becomes the button itself, so the padding is part of the target and the focus ring is drawn around what the reader sees — and a removable chip does not end up with a button inside a button.
- Pass active on a chip that toggles, and leave it off one that does not. aria-pressed is read off that same value, so the accent fill and the state assistive tech hears cannot drift apart; omitted, nothing is announced, which is the right answer for a chip that navigates rather than toggles.
- Give a filter row an off state to come back to — active is the accent, and a row in which every tag is active spends the mark that means “this one” on all of them.
- Name the subject in removeLabel — "Remove Rust filter", not "Remove". It is required alongside onRemove because eight chips whose controls are all called Remove is eight controls a screen reader cannot tell apart.
Don’t
- It carries no tone at all, so a tag cannot say success or danger. Colouring one in through className puts a hue into the system by hand and leaves the accent as the only thing that still reads as selected.
- One tag on its own is a Badge that lost its tone. The component is built to be scanned in a row, and a single chip beside a record is one fact about one record.
- Do not wrap it in a button of your own. Around a removable chip that is a button inside a button — invalid markup a parser splits into siblings, leaving a DOM neither the author nor the accessibility tree expects. onClick is what that wrapper was for.
Examples
default
A facet row with one facet chosen. The accent fill is the system's one pointer at a choice, so a row needs an off state to come back to — every tag active spends the mark that means "this one" on all of them. Nothing here is clickable: without onRemove a Tag is a plain span, and the button, the focus ring and the aria-pressed that makes the selection audible belong at the call site, as the next example shows.
a filter row
The same row, filtering. The button is OUTSIDE the tag: the span carries the 10px of padding, so a button nested inside it is a target the size of the words with dead margin around it that still looks clickable. aria-pressed is set in the same breath as active, because active paints the accent and reaches assistive tech in no other way — without it the chosen facet is a state only a sighted reader can see.
tag badge or pill
The three chips that look nearly identical, on the one record where each is the right answer. A Tag names what the record is ABOUT, so several sit together and get scanned; a Badge is one fact about one record, which is why it carries the status tones and a tag carries none; a StatusPill is the live state of the view itself, at most one, in the loudest small type the system has. Reach for the tag when you would want a second one beside it.
a removable chip
The removable form: a chip for something the reader has already chosen. This is where a separate Token component would have gone, and the difference between the two is one prop rather than one component. removeLabel is required alongside onRemove rather than defaulting to "Remove", because eight controls all called Remove are eight controls a screen reader cannot tell apart — name the subject in it.
Accessibility
- Presentational until it is given a handler. onClick makes the chip a real button carrying aria-pressed from active, so the focus ring and the pressed state stay on the element that draws them.
- Both controls are real <button type="button">s and siblings, so Tab reaches each, Enter and Space fire each, and neither submits the form it happens to sit in.
- removeLabel is required with onRemove and is the button’s whole accessible name — the X itself is aria-hidden.