# Tag

A subject label — a topic, a technology, a filter facet — that filters with onClick and is dismissed with onRemove.

- Group: Display
- Import: `import { Tag } from '@misoto22/folio'`
- Page: https://ui.misoto22.com/components/tag/
- Related: badge, status-pill

## When to reach for it

Several sit together and the reader scans them. One fact about one record is a Badge. A chip the reader can toggle or dismiss is this one with onClick or onRemove, not a fourth component.

## Anatomy

- **Chip** (required) — 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.
- **Ground** (required) — --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.
- **Label** (required) — 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.

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

## Tag

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.

### Props

- `children` (required) — `ReactNode`.
- `active` — `boolean`. Renders the pressed/selected look, and declares the chip a toggle. An interactive chip reads `aria-pressed` off this same value, so the accent fill and the state a screen reader hears cannot drift apart. Leave it off for a chip whose `onClick` navigates or opens something: a control that is neither pressed nor unpressed is better announced as neither than as "not pressed".
- `onClick` — `() => void`. Makes the chip ITSELF the filter control. This prop exists because the alternative did not survive contact with `onRemove`. The advice used to be to wrap the tag in a button at the call site, which is fine on its own and invalid the moment the chip is also removable: the remove control is a real `<button>`, so the wrapper puts a button inside a button — markup the parser splits into siblings, leaving a DOM neither the author nor the accessibility tree expects. With this the wrapper is never written. The chip carries the click, the focus ring and the pressed state on the element that draws them, and a removable filter chip renders its two controls side by side instead of one inside the other.
- `onRemove` — `() => void`. Called when the reader dismisses the chip.
- `removeLabel` — `string`. The remove button's accessible name. Name the subject, not the action.

Also accepts: `Omit<HTMLAttributes<HTMLElement>, 'onClick'>`.

## Example — default

```tsx
import { Tag } from '@misoto22/folio'

<div className="flex flex-wrap items-center gap-2">
  <Tag active>All</Tag>
  <Tag>TypeScript</Tag>
  <Tag>Rust</Tag>
  <Tag>Photography</Tag>
</div>
```

## Example — a filter row

```tsx
import { Tag } from '@misoto22/folio'

<div className="flex flex-wrap items-center gap-2">
  {FACETS.map((facet) => (
    <button
      key={facet}
      type="button"
      aria-pressed={chosen.includes(facet)}
      onClick={() => toggle(facet)}
      className="rounded-(--radius-sm)"
    >
      <Tag active={chosen.includes(facet)}>{facet}</Tag>
    </button>
  ))}
</div>
```

## Example — tag badge or pill

```tsx
import { Badge, StatusPill, Tag } from '@misoto22/folio'

<div className="flex max-w-md flex-col gap-3">
  <div className="flex flex-wrap items-center gap-2">
    <span className="font-heading text-[length:var(--fs-item)] text-(--ink)">
      Retrieval rewrite
    </span>
    <Badge tone="success">Merged</Badge>
  </div>
  <div className="flex flex-wrap items-center gap-2">
    <Tag>TypeScript</Tag>
    <Tag>pgvector</Tag>
    <Tag>Search</Tag>
  </div>
  <StatusPill pulse={false}>Deployed to production</StatusPill>
</div>
```

## Example — a removable chip

```tsx
import { Tag } from '@misoto22/folio'

<div className="flex flex-wrap items-center gap-2">
  {chosen.map((facet) => (
    <Tag
      key={facet}
      onRemove={() => setChosen((current) => current.filter((item) => item !== facet))}
      removeLabel={`Remove the ${facet} filter`}
    >
      {facet}
    </Tag>
  ))}
  {chosen.length === 0 && <span className="text-sm text-(--ink-3-aa)">No filters</span>}
</div>
```
