Skip to content

Tag <l-tag>

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

html
<l-tag removable>Design</l-tag>
HTML tag<l-tag>
Native
Progressive
Plain
Shadow-DOM
Shadow-DOM Custom Element

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.

Design Development Removable
Code
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.

Small Medium Large
Code
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.

Marketing Jane Cooper
Code
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.

Any time Today This week This month
Code
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.

In stock 128 On sale 42 New arrivals 17
Code
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.

Locked
Code
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.

Category
Cameras 128 Lenses 54 Tripods 12
Brand
Acme 86 Globex 41 Initech 23 Umbrella 12 Vandelay 9
Code
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.

Delivered 128 Pending 42 Cancelled 7
Code
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

WCAG4.1.2
RGAA11.1
Target size

The remove button keeps a minimum 24×24px hit target, and a selectable chip is taller than a display one

WCAG2.5.8
RGAA13.10
Keyboard

The remove button is reachable with Tab and removes the tag with Backspace / Delete; a selectable tag toggles with Enter or Space

WCAG2.1.1
RGAA12.9
State

A selectable tag exposes aria-pressed, or the checked state of its checkbox with control=checkbox

WCAG4.1.2
RGAA7.1
Color contrast

The label keeps the minimum contrast ratio over the chip background in every state, including selected and disabled

WCAG1.4.3
RGAA3.2
Not colour alone

With control=checkbox the checkmark conveys selection alongside the tint

WCAG1.4.1
RGAA3.1

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

sizeTagSizedefault:'md'Property
Tag size: sm, md (default), or lg.
removablebooleandefault:falseProperty
Show a remove button (and enable Backspace/Delete removal).
selectablebooleandefault:falseProperty
Make the tag a filter control the user can toggle on and off.
selectedbooleandefault:falseProperty
Whether the tag is selected. Reflected, so [selected] is styleable.
controlTagControldefault:'none'Property
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.
disabledbooleandefault:falseProperty
Disable the tag — dims it and blocks selection and removal.

Events

removecancelableEvent
Fired when the user removes the tag (× click or Backspace/Delete). Cancelable; if not prevented the tag removes itself. Not composed, does not bubble.
changeEvent
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)Slot
The tag label.
prefixSlot
Leading content, e.g. an <l-icon> or <l-avatar>.
suffixSlot
Trailing content, e.g. a result count. Gets the chip's own gutter, so no margin is needed.

CSS parts

basePart
The chip container.
contentPart
The label wrapper.
togglePart
The toggle button rendered by selectable (not with control="checkbox").
checkboxPart
The native checkbox rendered by control="checkbox".
removePart
The remove button.

CSS custom properties

--border-radiusCustom property
Corner radius. Defaults to a full pill.
--heightCustom property
Chip height. Defaults to the size step (a selectable chip is taller, to keep a comfortable target).
--font-sizeCustom property
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-inlineCustom property
Horizontal padding. Defaults to the size step.
--backgroundCustom property
Chip background.
--colorCustom property
Text color.
--selected-colorCustom property
Text, border, and checkbox accent when selected. Defaults to the library's form-control accent, lightened in dark mode.
--selected-backgroundCustom property
Chip background when selected. Defaults to a tint of --selected-color.