---
outline: deep
---

# Tag

Tags are compact chips for tokens, filters, and selected values, with an optional remove button.

```html
<l-tag removable>Design</l-tag>
```

**`<l-tag>`** — Custom Element · Shadow DOM

## Options

### Removable

Add `removable` for a × button. The user can also press `Backspace` or `Delete` while it is focused. Each removal fires a cancelable `remove` event; if nothing calls `preventDefault()`, the tag removes itself from the DOM.

```html
<div class="flex flex-wrap items-center gap-2">
  <l-tag>Design</l-tag>
  <l-tag>Development</l-tag>
  <l-tag removable>Removable</l-tag>
</div>
```

### Sizes

Add `size="sm"` or `size="lg"`. Default is md. A removable tag is always at least 24px tall to keep its remove target accessible.

```html
<div class="flex flex-wrap items-center gap-2">
  <l-tag
    size="sm"
    removable
    >Small</l-tag
  >
  <l-tag removable>Medium</l-tag>
  <l-tag
    size="lg"
    removable
    >Large</l-tag
  >
</div>
```

### Leading and trailing content

Put an icon or avatar in the `prefix` slot, and a count or trailing glyph in the `suffix` slot. Both get the chip's own gutter, so they need no margin.

```html
<div class="flex flex-wrap items-center gap-2">
  <l-tag removable>
    <l-icon
      slot="prefix"
      name="mdi:tag-outline"
    ></l-icon>
    Marketing
  </l-tag>
  <l-tag removable>
    <l-icon
      slot="prefix"
      name="mdi:account-outline"
    ></l-icon>
    Jane Cooper
  </l-tag>
</div>
```

### Selectable

Add `selectable` to turn the chip into a filter control. The tag becomes a toggle button, and re-activating a selected tag deselects it — so a facet always has a way back to "all". Each toggle fires a `change` event carrying the new `selected` state.

```html
<div class="flex flex-wrap items-center gap-2">
  <l-tag
    selectable
    selected
    >Any time</l-tag
  >
  <l-tag selectable>Today</l-tag>
  <l-tag selectable>This week</l-tag>
  <l-tag selectable>This month</l-tag>
</div>
```

### Checkbox

Add `control="checkbox"` for a multi-select facet: the library's checkbox rides inside and the chip becomes its label, so clicking anywhere on the chip toggles it. Implies `selectable`.

```html
<div class="flex flex-wrap items-center gap-2">
  <l-tag
    selectable
    control="checkbox"
    >In stock <span slot="suffix">128</span></l-tag
  >
  <l-tag
    selectable
    control="checkbox"
    selected
    >On sale <span slot="suffix">42</span></l-tag
  >
  <l-tag
    selectable
    control="checkbox"
    >New arrivals <span slot="suffix">17</span></l-tag
  >
</div>
```

### Disabled

Add `disabled` to block removal. The label stays legible — disabled is conveyed by the dimmed × button and the `not-allowed` cursor, not by washing out the text.

```html
<div class="flex flex-wrap items-center gap-2">
  <l-tag
    removable
    disabled
    >Locked</l-tag
  >
</div>
```

## Examples

### Filter panel

One tag per facet value: a checkbox for a multi-select axis, the count in the `suffix` slot. Listen for `change` on the container — the event bubbles and carries `selected`.

```html
<div class="flex max-w-md flex-col gap-4">
  <div class="flex flex-col gap-2">
    <span class="text-xs font-medium text-[var(--l-color-text-secondary)]">Category</span>
    <div class="flex flex-wrap gap-2">
      <l-tag
        selectable
        control="checkbox"
      >
        <l-icon
          slot="prefix"
          name="mdi:camera-outline"
        ></l-icon>
        Cameras <span slot="suffix">128</span>
      </l-tag>
      <l-tag
        selectable
        control="checkbox"
        selected
      >
        <l-icon
          slot="prefix"
          name="mdi:circle-outline"
        ></l-icon>
        Lenses <span slot="suffix">54</span>
      </l-tag>
      <l-tag
        selectable
        control="checkbox"
      >
        <l-icon
          slot="prefix"
          name="mdi:tripod"
        ></l-icon>
        Tripods <span slot="suffix">12</span>
      </l-tag>
    </div>
  </div>

  <div class="flex flex-col gap-2">
    <span class="text-xs font-medium text-[var(--l-color-text-secondary)]">Brand</span>
    <div class="flex flex-wrap gap-2">
      <l-tag
        selectable
        control="checkbox"
        >Acme <span slot="suffix">86</span></l-tag
      >
      <l-tag
        selectable
        control="checkbox"
        >Globex <span slot="suffix">41</span></l-tag
      >
      <l-tag
        selectable
        control="checkbox"
        >Initech <span slot="suffix">23</span></l-tag
      >
      <l-tag
        selectable
        control="checkbox"
        >Umbrella <span slot="suffix">12</span></l-tag
      >
      <l-tag
        selectable
        control="checkbox"
        >Vandelay <span slot="suffix">9</span></l-tag
      >
    </div>
  </div>
</div>
```

### Theming the selected state

`--selected-color` sets the text, border, and checkbox accent at once, and the background is derived from it. Add `--selected-background` for a different fill, and `--border-radius` to trade the pill for a softer rectangle. Every custom property inherits, so setting them on the group themes each chip inside.

A dense filter drawer usually wants a step between `md` and `lg`: `--height` and `--font-size` (and `--padding-inline` if needed) land anywhere between them, so `::part(base)` stays out of it. The example below sits at 26px / 13px.

Pair a semantic text token with its matching `-soft` fill — those two are designed to clear 4.5:1 together in both light and dark. A `--selected-color` on its own must clear that bar against the tint the chip derives from it.

```html
<div
  class="flex flex-wrap items-center gap-2 [--border-radius:var(--l-radius-md)] [--font-size:13px] [--height:26px] [--selected-background:var(--l-color-bg-fill-success-soft)] [--selected-color:var(--l-color-text-success)]"
>
  <l-tag
    selectable
    control="checkbox"
    selected
    >Delivered <span slot="suffix">128</span></l-tag
  >
  <l-tag
    selectable
    control="checkbox"
    >Pending <span slot="suffix">42</span></l-tag
  >
  <l-tag
    selectable
    control="checkbox"
    >Cancelled <span slot="suffix">7</span></l-tag
  >
</div>
```

## Accessibility

### Criteria

- **Accessible name** — The remove button exposes a localized `Remove` label
- **Target size** — The remove button keeps a minimum 24×24px hit target, and a selectable chip is taller than a display one
- **Keyboard** — The remove button is reachable with Tab and removes the tag with Backspace / Delete; a selectable tag toggles with Enter or Space
- **State** — A selectable tag exposes `aria-pressed`, or the checked state of its checkbox with `control=checkbox`
- **Color contrast** — The label keeps the minimum contrast ratio over the chip background in every state, including selected and disabled
- **Not colour alone** — With `control=checkbox` the checkmark conveys selection alongside the tint

### Rules

- Always give a removable tag a visible text label so its remove button has context
- Group related selectable tags under a visible heading, and give the group a role of `group` with an `aria-label` when the heading is not adjacent
- Use `control=checkbox` for a multi-select axis and the default toggle for a single-value one — never radios, which cannot be released once checked

### Keyboard interactions

- `Tab` — Moves focus to the tag (when selectable), then to the remove button (when removable)
- `Enter / Space` — Toggles a focused selectable tag, or activates the focused remove button
- `Backspace / Delete` — Removes the tag while it is focused

### Selectors & testing

Selection state lives on the reflected `selected` attribute of the host — that is the one to query. `aria-pressed` sits on the toggle button and `control="checkbox"` renders its `<input>`, both **inside the shadow DOM**, so a descendant selector from the light DOM never matches them. Role queries do work: the accessibility tree is unaffected by the shadow boundary.

```js
document.querySelectorAll('l-tag[selected]'); // ✅ reflected boolean attribute
screen.getByRole('button', { pressed: true }); // ✅ selectable tag
screen.getByRole('checkbox', { checked: true }); // ✅ control="checkbox"

tag.querySelector('input'); // ❌ null — the checkbox is in the shadow root
tag.getAttribute('aria-pressed'); // ❌ null — it is on the inner button
```

Activation goes through the host, so a test runner clicks the chip it resolved by `data-testid` — no reaching into the shadow root, and no custom command.

```js
tag.click(); // ✅ toggles a selectable chip (does not remove a removable one)
```

```css
/* Style by the reflected attribute; ::part() reaches the inner nodes. */
l-tag[selected]::part(base) {
  outline: 1px dashed var(--l-color-border);
}
```

### Controlled usage

The chip is uncontrolled: it applies the new state, then fires `change`. To veto a toggle — a "max 3 filters" rule, an async guard — set `selected` back in the listener. The revert lands in the same render, so nothing is painted in between.

```js
panel.addEventListener('change', (event) => {
  const tag = event.target;
  if (tag.selected && selectedCount() > 3) tag.selected = false;
});
```

## API reference

### Importing

```js
import 'luxen-ui/tag';
```

### Attributes & Properties

- **size**: `TagSize` (default: `'md'`) — Tag size: `sm`, `md` (default), or `lg`.
- **removable**: `boolean` (default: `false`) — Show a remove button (and enable Backspace/Delete removal).
- **selectable**: `boolean` (default: `false`) — Make the tag a filter control the user can toggle on and off.
- **selected**: `boolean` (default: `false`) — Whether the tag is selected. Reflected, so `[selected]` is styleable.
- **control**: `TagControl` (default: `'none'`) — What drives the selection: `none` (default) makes the chip itself a toggle
button; `checkbox` renders a checkbox inside and makes the chip its label —
the right choice for a multi-select facet. Implies `selectable`.
- **disabled**: `boolean` (default: `false`) — Disable the tag — dims it and blocks selection and removal.

### Events

- **remove** (cancelable) — Fired when the user removes the tag (× click or Backspace/Delete). Cancelable; if not prevented the tag removes itself. Not composed, does not bubble.
- **change** — Fired when a `selectable` tag is toggled. Not cancelable — like the platform's own `change`, and like `l-segmented-control`. Bubbles. Properties: `selected: boolean`. The chip is uncontrolled: it applies the new state before dispatching, so a host that vetoes a toggle (a "max 3 filters" rule, an async guard) sets `selected` back in the listener — the revert lands in the same render and is never painted.

### Slots

- **(default)** — The tag label.
- **prefix** — Leading content, e.g. an `<l-icon>` or `<l-avatar>`.
- **suffix** — Trailing content, e.g. a result count. Gets the chip's own gutter, so no margin is needed.

### CSS parts

- `base` — The chip container.
- `content` — The label wrapper.
- `toggle` — The toggle button rendered by `selectable` (not with `control="checkbox"`).
- `checkbox` — The native checkbox rendered by `control="checkbox"`.
- `remove` — The remove button.

### CSS custom properties

- `--border-radius` — Corner radius. Defaults to a full pill.
- `--height` — Chip height. Defaults to the `size` step (a selectable chip is taller, to keep a comfortable target).
- `--font-size` — Label size. Defaults to the `size` step — 12px, or 14px at `size="lg"`. Set it with `--height` to land between the two steps in a dense filter panel.
- `--padding-inline` — Horizontal padding. Defaults to the `size` step.
- `--background` — Chip background.
- `--color` — Text color.
- `--selected-color` — Text, border, and checkbox accent when selected. Defaults to the library's form-control accent, lightened in dark mode.
- `--selected-background` — Chip background when selected. Defaults to a tint of `--selected-color`.
