Tag

Variants

Default Accent Danger Success Muted

Sizes

Small Medium

Dismissible

React Featured JavaScript

Skill tag stack

TypeScript React CSS Node.js PostgreSQL

  
/*
 * Tag — element (display)
 *
 * Inline metadata pill. Like Badge but optionally dismissible. The
 * dismiss button is an opt-in <button> inside the tag; tag dismiss
 * logic is the consumer's responsibility (remove the DOM node from a
 * click handler).
 *
 *   <span class="tag">Documentation</span>
 *   <span class="tag -accent">Featured</span>
 *
 *   <span class="tag">
 *     React
 *     <button type="button" class="tag__dismiss" aria-label="Remove React">×</button>
 *   </span>
 */

@layer malevich.components {
  .tag {
    display: inline-flex;
    align-items: center;
    gap: var(--space-gap-elements-s);

    padding-block: var(--space-inset-element-s);
    padding-inline: var(--space-inset-element-m);

    background-color: var(--color-surface-raised);
    color: var(--color-ink-strong);

    border: var(--border-width-hairline) solid var(--color-border-muted);
    border-radius: var(--radius-pill);

    font-family: var(--font-body-support-family);
    font-size: var(--font-body-support-size);
    font-weight: 500;
    line-height: var(--font-body-support-line-height);
    letter-spacing: var(--font-body-support-letter-spacing);

    white-space: nowrap;
    max-inline-size: 100%;
  }

  .tag__dismiss {
    appearance: none;
    -webkit-appearance: none;
    background: transparent;
    border: 0;
    padding: 0;
    margin: 0;
    cursor: pointer;

    display: inline-flex;
    align-items: center;
    justify-content: center;

    inline-size: var(--size-control-s);
    block-size: var(--size-control-s);
    margin-inline-end: calc(-1 * var(--space-inset-element-s));

    color: var(--color-ink-subtle);
    border-radius: 50%;
    font-family: inherit;
    font-size: inherit;
    line-height: 1;

    transition: color var(--motion-fast) var(--motion-easing-default),
                background-color var(--motion-fast) var(--motion-easing-default);
  }

  .tag__dismiss:hover {
    color: var(--color-ink-strong);
    background-color: var(--color-surface-canvas);
  }

  .tag__dismiss:focus-visible {
    outline: var(--border-width-focus) solid var(--color-accent);
    outline-offset: var(--border-width-hairline);
  }

  /* Variants */

  .tag.-accent {
    background-color: var(--color-accent-muted);
    color: var(--color-accent-strong);
    border-color: var(--color-accent-muted);
  }

  .tag.-danger {
    background-color: var(--color-surface-raised);
    color: var(--color-danger);
    border-color: var(--color-danger);
  }

  .tag.-success {
    background-color: var(--color-surface-raised);
    color: var(--color-success);
    border-color: var(--color-success);
  }

  .tag.-muted {
    background-color: var(--color-surface-canvas);
    color: var(--color-ink-regular);
  }

  .tag.-s {
    padding-block: 0;
    padding-inline: var(--space-inset-element-s);
    font-size: var(--font-caption-size);
  }
}
This component is pure CSS — no JavaScript required.
# Tag — AGENTS.md

> Auto-generated from `tag.docs.md` + the modifier applicability matrix.
> Edit those source files; this file regenerates on build.

## Identity

- **Component:** `tag`
- **Layer:** elements
- **Status:** stable
- **Last updated:** 2026-05-19

## Summary

- Categorize content: "React", "Design", "Tutorial".

## Variants

| Variant | Class            | Use for                                |
|---------|------------------|----------------------------------------|
| Default | `.tag`           | Neutral metadata                       |
| Accent  | `.tag.-accent`   | Highlighted (selected, featured)       |
| Danger  | `.tag.-danger`   | Error or critical tags                 |
| Success | `.tag.-success`  | Positive state tag                     |
| Muted   | `.tag.-muted`    | De-emphasized                          |
| Small   | `.tag.-s`        | Compact size                           |

## Modifier applicability

Per ADR-0007. Modifiers attach via `data-{category}="value"` attributes.

| Category | Accepts |
|---|---|
| `background` | — |
| `surface` | `flat` |
| `effect` | — |
| `shader` | — |
| `rhythm` | — |
| `motion` | any value |
| `behavior` | `dismissible` |

## Anatomy

```html
<!-- Static -->
<span class="tag">Documentation</span>
<span class="tag -accent">Featured</span>

<!-- Dismissible -->
<span class="tag">
  React
  <button type="button" class="tag__dismiss" aria-label="Remove React">×</button>
</span>

<!-- In a group -->
<div style="display:flex; gap:0.5rem; flex-wrap:wrap;">
  <span class="tag -muted">JavaScript</span>
  <span class="tag -muted">CSS</span>
  <span class="tag -muted">HTML</span>
</div>
```

Click the dismiss `<button>` to handle removal:

```js
document.addEventListener("click", (e) => {
  const dismiss = e.target.closest(".tag__dismiss");
  if (dismiss) dismiss.closest(".tag")?.remove();
});
```

The component does not ship this handler — removal policy varies
(remove from server, mark as removed locally, undo affordance).

## Tokens consumed

- `--color-surface-raised` — default background
- `--color-surface-canvas` — muted background
- `--color-accent-muted`, `--color-accent-strong` — accent variant
- `--color-danger`, `--color-success` — status variants
- `--color-ink-strong`, `--color-ink-regular`, `--color-ink-subtle`
- `--color-border-muted` — default border
- `--radius-pill` — corner radius
- `--space-inset-element-{s,m}` — padding
- `--space-gap-elements-s` — gap before dismiss
- `--size-control-s` — dismiss button size
- `--border-width-hairline` / `--border-width-focus`
- `--font-body-{s,xs}-*` — typography
- `--motion-fast` / `--motion-easing-default`

## Accessibility contract

- Tags are decorative inline elements. Use `<span>` unless the tag is
  interactive (links to a filter view → use `<a>`).
- The dismiss button is a real `<button>` with `aria-label` so
  screen readers announce "Remove React".
- When removing a tag from the DOM, focus moves to the next sibling
  or back to a container (consumer-defined). The component does not
  manage focus on removal.

## Guidance

### Do

- Use `<span>` for static tags, `<a>` for navigable tags.
- Always pair the dismiss button with a descriptive `aria-label`.
- Keep tag labels short — 1-3 words.

### Don't

- Don't make the entire tag clickable AND include a dismiss button —
  the click targets conflict.
- Don't use `<button>` as the outer element. The dismiss is the
  interactive part, the tag itself is text.