<!-- Generated by scripts/build-agent-kit.ts for @aistrike-dev/ui@5.0.1. Do not edit. -->

# Chip vs Badge vs TableStatusCell vs CardDataTag

<!-- use-when: A small pill or count with short text in it: which one, and what does clicking it do? -->

A rounded container with a couple of words in it is the most overloaded shape in any product UI. The
same pill can be a severity indicator, a workflow state, a category, a removable filter, a toggle, or
a menu trigger — six behaviours, one appearance. Naming these by shape is what produces the confusion,
which is why this design system does not ship a `Tag`, a `Label`, a `Lozenge` or a `Pill`.

The question that separates them is **"is it attached to something else, and what happens when the
user clicks it?"** Answer that and the component and its `purpose` both fall out.

| Element | It is attached to | The click means | What you use |
| --- | --- | --- | --- |
| Count or dot on an icon, tab, nav item | A parent element | Nothing — the parent owns the click | `Badge` |
| Standalone pill anywhere else | Nothing | Depends on the purpose | `Chip` |
| Lifecycle state in a table row | The row | Nothing | `TableStatusCell` |
| Entity value inside card copy | The sentence | Nothing | `CardDataTag` |

`Badge` rides on a parent and is never clickable. `Chip` stands on its own. That single distinction is
the one every major design system agrees on.

## Decision flow

```
Does it ride on a parent element as a count or a dot?
        │
        ├── Yes ──────────────────────────────────► Badge
        └── No
             │
             ├── Is it inside a Table row?
             │     ├── Workflow state ────────────► TableStatusCell
             │     └── Anything else ─────────────► Chip size="small"
             │
             ├── Is it a non-interactive entity in card copy?
             │     └── Yes ────────────────────────► CardDataTag
             │
             └── Otherwise ────────────────────────► Chip, purpose by meaning
                       │
                       ├── Risk level ────────────► purpose="severity"
                       ├── Workflow / health ─────► purpose="status"
                       ├── Classification ────────► purpose="category"
                       ├── Measured value ────────► purpose="metric"
                       ├── Feature maturity ──────► purpose="lifecycle"
                       ├── Removable criterion ───► purpose="filter"
                       ├── Toggleable option ─────► purpose="select"
                       ├── Picks values from a set► FilterMenu (purpose="picker")
                       ├── Entity in prose ───────► purpose="entity"
                       └── "+N more" ─────────────► purpose="overflow"
```

## Chip

`Chip` is the only general-purpose compact label. You never choose its appearance — you choose a
`purpose` from what the value *means*, and the appearance, the rendered element and the keyboard
contract are derived from it. The component reference entry carries the full rules for each purpose.

**Reach for it when:** a severity or risk level, a workflow or health state, category metadata, a
measured value, how mature a feature is, an applied filter, a toggleable option, a filter field the
user picks values from, an entity value in prose, or an overflow disclosure.

**Three appearance families, and which one a purpose lands in is not arbitrary.** Severity, status and
entity verdicts are *tinted*, because their colour is the information and they are meant to catch the
eye. `lifecycle` is the lone brand-accented outline, because a release marker must not read as a data
pill. Every neutral pill — `category`, `metric`, `filter`, `picker` and `overflow` — wears the same
chrome as an outlined TextField: a quiet fill inside a quiet border, sharing `inputColors` with the
inputs and the secondary Button. That is deliberate. A filter bar is a row of form controls, and the
pills in it are things the user reads *past* on the way to the data, so they should sit at the same
visual level as the controls beside them rather than competing with the severity chips in the table
below.

`filter` and `picker` go further and share a label treatment: the field name in muted ink, the value
promoted. Two chips in a bar then differ where they actually differ — on the value — instead of making
the eye read both pills end to end.

```tsx
import { Chip } from '@aistrike-dev/ui';

<Chip purpose="severity" level="critical" />
<Chip purpose="status" tone="warning" label="Investigating" />
<Chip purpose="category" label="Credential Access" size="small" />
<Chip purpose="lifecycle" stage="beta" size="small" />
<Chip purpose="filter" field="Severity" label="Critical" onRemove={clear} />
<Chip purpose="select" label="EDR" selected={on} onToggle={setOn} />
<Chip purpose="entity" kind="ip" value="203.0.113.47" verdict="malicious" onOpen={open} />
```

The five static purposes (`severity`, `status`, `category`, `metric`, `lifecycle`) render no tab stop
and no click handler. The five interactive ones (`filter`, `select`, `entity`, `overflow`, `picker`)
carry a focus ring, a hover response, the right ARIA state, and an affordance of their own: an X on
`filter`, a chevron on `picker`, `+N more` on `overflow`. That combination is not decoration — it is
how a user tells at a glance which pills in a view can be operated.

**The border is not part of that signal**, which is worth stating because it is the intuitive guess.
Most neutral pills draw one whether they are operable or not, since they share the chrome of an
outlined input. Using outline weight to mean "clickable" would put a filter chip and the picker beside
it in different visual species when all that differs is what you can do to them, so the affordance
carries it instead. `lifecycle` is the one purpose held apart by colour alone, using a hue no other
purpose touches.

**Practical limits:** the props are a discriminated union, so `onRemove` is required on `filter` and
unavailable on `severity`, and a wrong combination will not compile. `severity` derives its word from
`level`, so do not also pass a label. Use `size="small"` in tables and dense cells. Beside a Button,
pick the Chip rung that shares its height: Chip `medium` with Button `small`, Chip `large` with
Button `medium`, Chip `extraLarge` with Button `large`. Two chips per
paragraph is the practical ceiling for inline use, and `entity` is the only purpose that sizes itself
from the surrounding text rather than the size scale — it does not accept `size`, so to change an
entity chip's size, change the `Typography` around it.

### Release stages: `purpose="lifecycle"`

`New`, `Beta` and `Deprecated` describe **the product surface**, not the content in it. That is the
whole distinction, and it is the one that gets muddled: a `status` chip describes a record and a user
may act on it, while a stage changes only when engineering ships something. Keeping them apart is why
this is its own purpose rather than a tone on `status`.

```tsx
<Chip purpose="lifecycle" stage="beta" size="small" />
<Chip purpose="lifecycle" stage="deprecated" size="small" />
```

The vocabulary is closed — `new`, `beta`, `alpha`, `preview`, `deprecated` — and the word is derived
from `stage`, so one feature cannot read as "Beta" in the nav and "BETA" on its own page. `label`
exists for a product with its own vocabulary, not as an opening for `Coming soon`, which is a roadmap
claim rather than a state the UI is in.

**The test to apply:** could a user filter a list by the word? If yes it is data about a record and
belongs in `purpose="category"`. `Built-in` on a workflow row is a property of that workflow — a
column you could sort by — not a release stage, so it stays a neutral category chip. This is the
mistake worth catching, because "Built-in" and "Beta" feel like the same kind of label and are not.

**Appearance:** this is the only chip that carries the brand accent, and it carries it on the outline
rather than a fill. An outlined pill in a hue no severity level uses reads as "not one of the data
pills" without adding a sixth colour to the scale severity owns. All five stages share the one
colour: the colour says *release marker* and the word says which one. `deprecated` is the first
candidate for a treatment of its own, since turquoise currently gives a retiring feature the same
tint as a new one.

**Practical limits:** one per surface, on the name of the thing that is new — a nav item, a page
title, a settings row. Remove it when the feature ships; a permanent `New` teaches users to ignore
the treatment, and removing it is part of the launch rather than a follow-up.

### Picking values: `FilterMenu` and `purpose="picker"`

A `filter` chip shows a criterion the user can only remove. When they should be able to *change* it
without going somewhere else, that is a picker: the chip names the field and its current selection,
and clicking it opens a panel of checkboxes. Use `FilterMenu`, which owns the chip, the panel and the
open state. `Chip purpose="picker"` is the bare trigger underneath it, for the rare case where you are
supplying your own surface.

```tsx
import { FilterMenu } from '@aistrike-dev/ui';

<FilterMenu
  field="Severity"
  options={[{ label: 'Critical', value: 'critical' }, { label: 'High', value: 'high' }]}
  value={severities}
  onChange={setSeverities}
  onRemove={() => dropFilter('severity')}
/>
```

This is the one chip that both carries a value and changes it, which is a deliberate exception to
"a chip is not an action trigger". The line that keeps the rule intact: **a picker changes which
values are filtered on, never the record underneath.** Narrowing a table to critical alerts is a
filter; setting one alert to `Contained` is still a `Menu` behind a `Button`.

It is also the one chip allowed two targets — the body opens the panel, the X removes the filter.
Both operate on the same filter, which is what makes it legible where a body that edits beside a
corner that removes would not be.

**The X removes; it does not clear.** It is the same glyph in the same corner as on
`purpose="filter"`, so it has to mean the same thing there: the criterion leaves the bar. Emptying
the selection while keeping the field is the panel's `Reset`, which needs no prop — that division is
what stops the two controls duplicating each other.

Removal is the parent's to perform, since a chip cannot take itself out of a list it does not own, so
`onRemove` is where you drop the field from your own state. Pass it only where the user can get the
field back, through an `Add filter` menu or similar; for a bar of fixed fields, omit it and let
`Reset` do the emptying.

**Capturing the panel's lifecycle:** `onOpen` and `onClose` fire when the panel opens and closes, and
`onClose` receives the selection as applied at that moment. That is the hook for running one query
per visit rather than one per checkbox while keeping the live feedback of `commit="immediate"`: track
the selection from `onChange`, and refetch in `onClose`. Prefer it over `commit="apply"` when the only
problem is query cost rather than a misleading partial result.

**The two summary modes** exist because option labels differ wildly in length. The default names the
first selection and counts the rest, so `Severity: Critical +1` means two are on — the counter counts
what the first value hides. `summary="count"` names none of them and shows `Datasources: 3`, which is
what you want when a value like `classifyout-saviynt_subscriber-saviynt-v4` would push the pill past
the width of the bar and tell the user nothing more.

**Practical limits:** selections apply live by default. Use `commit="apply"` when each change costs a
query the user should not pay for mid-thought, or when a partial selection would show a misleading
result — then the panel gains an Apply button and the chip keeps showing what is *applied* rather than
what is ticked. Add `searchable` past about ten options and `showSelectAll` when picking most of them
is normal. Group options with `option.group` when the same name can come from more than one source.
Order `options` the way you want them read: selections are held in option order, not click order, so
the chip's first value does not depend on which box was ticked first.

## Badge

Use `Badge` only for a count or a dot **anchored to another element** — an icon, a tab, a nav item.
Anchoring is its entire identity: if there is no parent for it to sit on, you wanted
`Chip purpose="metric"`.

```tsx
import { Badge } from '@aistrike-dev/ui';
import NotificationsIcon from '@mui/icons-material/Notifications';

<Badge badgeContent={12} color="error">
  <NotificationsIcon />
</Badge>
```

**Practical limits:** the badge takes no focus and no click handler of its own — the parent owns
those. A bare number is meaningless to a screen reader, so the definition of the count belongs in the
parent's accessible name (`Unassigned alerts, 12`). Cap large values consistently and decide
deliberately whether zero shows.

## TableStatusCell

Inside a `Table`, workflow state is a dot plus a label rather than a pill. A row already carries
severity chips and facet chips, and a third pill shape in the same row stops reading as a distinct
dimension. (Workflow state, not `purpose="lifecycle"` — a row is a record, and a record has no
release stage.)

```tsx
import { TableStatusCell } from '@aistrike-dev/ui';

<TableStatusCell label="Investigating" tone="warning" />
```

**Practical limits:** this applies to workflow state only. Severity in the same row is still
`Chip purpose="severity"`, and facets are still chips, grouped through `ChipGroup`.

## CardDataTag

A static, monospace, inline pill for an entity mentioned in card copy — a hostname, a user, an IP.
Use it when the value is worth setting apart typographically but there is nothing to click.

```tsx
import { CardDataTag } from '@aistrike-dev/ui';

<Typography variant="body1">
  Escalation observed on <CardDataTag>WIN-DC-02</CardDataTag>.
</Typography>
```

**Practical limits:** it has no interaction, no kind, and no verdict. The moment the analyst needs to
copy the value or pivot on it, switch to `Chip purpose="entity"`, which is the interactive form of
the same idea.

## Entities in prose: EntityMenu and AnalystMarkdown

`Chip purpose="entity"` is the only purpose that is half of a pair. The chip is the target; the menu
is where the value actually becomes useful. A chip that opens nothing is a `CardDataTag` with a
misleading focus ring, so if you render an entity chip you owe it an `onOpen`.

`EntityMenu` is that menu, and its three regions are fixed so the same value reads the same
everywhere: **identity** (kind and full canonical value), **attributes** (verdict and enrichment
facts), then **actions**. `Copy value` is always present, and for `ip`, `domain`, `url` and `email`
a `Copy defanged` action appears automatically — analysts paste indicators into tickets and chat,
and a live URL in a ticket is a hazard. Your own pivots are appended after those.

```tsx
import { Chip, EntityMenu } from '@aistrike-dev/ui';

const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);

<Chip purpose="entity" kind="ip" value="203.0.113.47" verdict="malicious"
      onOpen={(event) => setAnchorEl(event.currentTarget)} />

<EntityMenu
  entity={{ kind: 'ip', value: '203.0.113.47', verdict: 'malicious',
            attributes: [{ label: 'First seen', value: '2 hours ago' }] }}
  anchorEl={anchorEl}
  open={Boolean(anchorEl)}
  onClose={() => setAnchorEl(null)}
  actions={[{ id: 'pivot', label: 'Pivot to search' }]}
  onAction={(id, entity) => pivot(id, entity)}
/>
```

`AnalystMarkdown` applies the same pair across a whole AI-written block: it renders the markdown and
turns declared entity spans into entity chips wired to one shared menu. It lives behind a subpath so
that projects with no markdown to render do not pay for the renderer, and it needs `react-markdown`
and `remark-gfm` installed alongside.

```tsx
import { AnalystMarkdown } from '@aistrike-dev/ui/markdown';

<AnalystMarkdown
  source={summary}
  entities={spans}
  aiGenerated
  entityActions={[{ id: 'pivot', label: 'Pivot to search' }]}
  onEntityAction={(id, entity) => pivot(id, entity)}
/>
```

**Practical limits:** spans are `{ start, end, kind, value }` offsets into the raw source, and they
must come from the model or the enrichment service. **Never regex the prose to find them** — a
pattern that turns every dotted string into an IP will mark version numbers and file names, and a
wrong verdict tint is worse than no chip. Spans inside code, links and headings are ignored on
purpose, so an indicator in a command stays copyable as text. Marking is capped per block
(`maxMarkedPerBlock`, default 8) because a paragraph of pills is unreadable; mark the first mention
and leave later ones as prose. Raw HTML in the source is rendered as text, never parsed, since this
content is model output. Use `aiGenerated` for one marker on the block — never one per chip.

## ChipGroup

Whenever more than two chips sit together, wrap them in `ChipGroup`. It owns the spacing, the
wrapping and the overflow, so the chips never set their own margins and every group truncates the
same way.

```tsx
import { Chip, ChipGroup } from '@aistrike-dev/ui';

<ChipGroup label="Techniques" max={3}>
  {techniques.map((t) => <Chip key={t} purpose="category" label={t} size="small" />)}
</ChipGroup>
```

**Practical limits:** six or fewer chips on one line, wrapping to at most five lines; past that the
content wants a multi-select or a table. In a table pass `onOverflow` so the remainder opens a panel
instead of expanding in place and changing the row height.

## Worked examples

| Scenario | What to use | Why |
| --- | --- | --- |
| Severity column in a findings table | `Chip purpose="severity" size="small"` | Reserved scale, canonical word derived from `level` |
| Workflow state column in the same table | `TableStatusCell` | Workflow state in a row is a dot plus label |
| Same workflow state on a detail page | `Chip purpose="status"` | Outside a row, the pill form is right |
| MITRE techniques on an alert | `ChipGroup` of `purpose="category"` | Neutral metadata, capped and wrapped |
| "12 related entities" in a cell | `Chip purpose="metric"` | A measured count with nothing to anchor to |
| `Beta` next to a nav item or page title | `Chip purpose="lifecycle"` | Describes the feature's maturity, not its content |
| `Built-in` on a workflow row | `Chip purpose="category"` | A filterable property of that record, so it is data |
| Unread alert count on a nav item | `Badge` | Rides on the nav item, which owns the click |
| Applied filters above a table | `Chip purpose="filter"` | Removable criteria, field kept with value |
| Those filters, if the user can also change them | `FilterMenu` | The chip both shows the value and picks it |
| `Clear all` beside those filters | `Button variant="ghost"` | An action, not a criterion |
| Several filter fields committed together | `FilterPopover` | One trigger, one Apply, many fields |
| Severity options inside a filter panel | `Chip purpose="select"` | Toggles a selection, needs `aria-pressed` |
| Independent on/off options in a chip bar | `Chip purpose="select"` per option | Each chip owns its pressed state; neighbours are unaffected |
| Mutually exclusive options in a chip bar | `Chip purpose="select"` with one parent value | Clicking one selects it and clears the others |
| An IP address in an AI-written summary | `Chip purpose="entity"` | Analyst needs to copy it and pivot on it |
| A hostname in static card copy | `CardDataTag` | Typographic distinction, nothing to click |
| Chart legend that toggles a series | `Chip purpose="select"` | The click changes what is plotted |
| Chart legend that only labels series | `Chip purpose="category"` | Descriptive, so nothing to operate |
| Changing an alert's status inline | A `Menu` behind a `Button` | A chip is not an action trigger |
| A full SHA-256 in a details list | Monospace `Typography` | Too long for a pill; exact copying matters more |

## Anti-patterns

- **A `Badge` with no anchor.** A count floating in a cell or a menu row has no parent to attach to,
  which is what `Chip purpose="metric"` is for.
- **A `Chip` as an action trigger.** Chips carry values, not verbs. `Isolate host` is a `Button`, and
  changing a status in place is a `Menu` behind one. A picker chip is not the exception to this: it
  changes which values are filtered on, which is still a value, not a verb.
- **Picking `variant` and `color` directly.** That is the deprecated API; it permits forty
  combinations of which about eight are correct, and it warns in development. Pass `purpose`.
- **Borrowing the severity scale.** `critical | high | medium | low` means risk and nothing else.
  Verdict, confidence, health and chart series each have their own tokens.
- **Colour-coding categories.** In a security UI colour is already spent on severity; a rainbow of
  category chips beside a severity column makes both harder to read. Categories are neutral.
- **An interactive chip inside another click target.** A `filter` or `select` chip in a `MenuItem` or
  a linked table row creates two overlapping targets and an ambiguous result.
- **One chip with two functions.** A body that edits and a corner that removes means the outcome
  depends on where the pointer landed. The single exception is `purpose="picker"`, where both targets
  act on the same filter — and even there, drop the X when a `Clear all` already exists.
- **A static-looking pill that navigates.** If it goes somewhere, it needs a link affordance; better
  still, put `Open entity page` in an entity menu where it is named.
- **An entity chip with no menu.** The focus ring and `aria-haspopup` promise something to open. If
  there is nothing to open, the value was static and wanted `CardDataTag`.
- **A bare picker chip with no panel.** Same failure, same reason: the chevron and `aria-expanded`
  promise a panel. Use `FilterMenu` unless you are genuinely supplying your own.
- **A picker chip whose label shows raw values.** The chip prints what you give it, so `sev_1` reaches
  the user. Pass the labels, not the option values.
- **A `New` chip that never goes away.** A stage marker left on after launch trains users to ignore
  the treatment, so the next real `Beta` gets skipped too. Removing it is part of shipping.
- **A lifecycle chip on a record.** Stages describe the product surface. A row, a chart series or an
  alert has no release stage — if the word is filterable it is a `category`.
- **Truncating severity or status.** These never truncate and never wrap to a second line. Widen the
  column instead.
- **Chips on chart axis labels.** Axis labels rotate and truncate; a pill there is an unreadable
  smear. Carry the emphasis in the series colour and explain it in the legend.

## Accessibility

- Static purposes get no `tabIndex` and no click handler. Never add either to make a label look
  interactive — pick the interactive purpose instead.
- Each interactive purpose brings its own ARIA state: `select` sets `aria-pressed`, `entity` sets
  `aria-haspopup="menu"`, `overflow` sets `aria-expanded`, and `picker` sets `aria-haspopup="dialog"`
  plus `aria-expanded`. You do not set these yourself.
- `picker` spells the whole selection into its accessible name, because the visible text shows only the
  first value and hides the rest behind `+N`. Its panel is a named dialog holding real checkboxes, not
  a menu of `menuitemcheckbox` rows — a menu's role cannot legally contain the search field or the
  Apply button, and native checkboxes announce their own state.
- `filter` chips are removed by pointer on the delete icon or by `Backspace`/`Delete` on the focused
  chip. After removal, move focus predictably to the next chip, the previous chip, or the owning
  input — never let it fall to the page body.
- `entity` puts the full canonical value and the verdict into the accessible name, because the visible
  text may be truncated and the tint is not perceivable to everyone.
- Never rely on colour alone. Every purpose renders a visible word, and selected state reads as fill
  plus `aria-pressed`, not hue.
- Interactive chips need `24 x 24` CSS pixels or the spacing exception, and at least `8px` between
  neighbours — which is what `ChipGroup` gives you by default.

## When you are unsure, ask

The boundary between a filter, a toggle and a plain category is genuinely fuzzy in a mockup, and so is
the line between a status you can change and one you can only read. **If you cannot confidently pick
the purpose, stop and ask before building — state the decision you are weighing, the option you are
leaning toward, and why.**

Ask in concrete terms, not in the abstract:

> "The severity pills above this table look like applied filters. I'm treating them as
> `purpose="select"` so clicking one narrows the table. Did you intend them as read-only
> `purpose="category"` labels describing the current view instead?"

> "This row shows `Investigating`. Inside the table I'd use `TableStatusCell`, but if analysts are
> meant to change the status from here it needs a `Menu` behind a `Button` rather than any chip. Is it
> read-only?"

> "These chips above the table show the applied severity and priority. I'm building them as
> `FilterMenu` so clicking one reopens its options. Did you mean them as `purpose="filter"` — display
> only, removable, with the picking done elsewhere?"

A single clarifying question is far cheaper than shipping a pill whose behaviour surprises the user.

## References

- **Atoms → Chip** — the purpose ladder, with a story per purpose plus header, inline-prose and
  filter-bar placements.
- **Molecules → ChipGroup** — capping, wrapping and overflow.
- **Molecules → EntityMenu** — the three regions, and copy plus defang.
- **Molecules → FilterMenu** — the picker chip and its panel, both commit modes.
- **Guidelines → Choosing → Filter Controls** — FilterMenu vs FilterPopover vs FilterPanel.
- **Markdown → AnalystMarkdown** — entity spans in AI-written prose.
- **Atoms → Badge** — anchored counts and dots.
- **Organisms → Table → Cells** — `TableStatusCell` and the facet cells.
- **Organisms → Card** — `CardDataTag` in context.
- [MUI Chip](https://mui.com/material-ui/react-chip/) · [MUI Badge](https://mui.com/material-ui/react-badge/)
