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

Tag or badge?

A tag is a chip the user operates — selects, removes. For a chip the user only reads, including a categorizing one, use <l-badge>: it is lighter (light DOM, so it renders without its script), shorter by 2px, and it takes the same --text-color / --background-color / --border-color trio.

Options ​

Removable ​

Add removable for a remove button. It is a full-height segment at the end of the chip, split off by a rule, so the affordance is drawn at rest — touch and keyboard users never get a hover state, and a row of chips has to say where to click before the pointer arrives.

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

Only the segment reacts to the pointer. A removable tag has exactly one target, so the label is left alone — a tint spreading over the whole chip would promise a click that does nothing. Add selectable and the chip becomes a control in its own right, and then it does hover as a whole.

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>

Applied filters ​

The pattern removable tags exist for: every criterion the user has applied, named and individually revocable, above the results they narrow.

Colour carries meaning here — a chip's tint says what kind of criterion it is (a site, a department, a plain attribute), which is what lets someone scan the bar instead of reading it. Three custom properties cover it: --text-color, --background-color, and --border-color, which sets the chip's line and the rule in front of the remove button. Left alone, that line is derived from --text-color; set it and it holds on hover, so a themed chip keeps its own value rather than drifting toward a tint of its text. Selecting a chip still replaces it, along with the fill and the ink — --selected-border-color themes that step.

--border-radius inherits, so setting it once on the group trades the pill for a rectangle — the remove segment follows the corner either way.

Filtered by
Lyon branch Facilities LEASE-2025-001
Code
html
<style>
  /* The colour of a chip says what kind of criterion it is. Three custom
     properties per kind — no ::part(), no wrapper component. */
  #demo-applied-filters .criterion {
    --border-radius: var(--l-radius-md);
  }
  #demo-applied-filters .criterion-site {
    --background-color: var(--l-color-bg-fill-success-soft);
    --text-color: var(--l-color-text-success);
    --border-color: color-mix(in oklab, var(--l-color-text-success) 35%, transparent);
  }
  #demo-applied-filters .criterion-department {
    --background-color: var(--l-color-bg-fill-warning-soft);
    --text-color: var(--l-color-text-warning);
    --border-color: color-mix(in oklab, var(--l-color-text-warning) 35%, transparent);
  }
</style>

<div
  id="demo-applied-filters"
  class="flex flex-wrap items-center gap-x-3 gap-y-2 rounded-[var(--l-radius-md)] border border-divider bg-surface px-3 py-2"
>
  <span class="text-[11px] font-medium uppercase tracking-wider text-tertiary">Filtered by</span>

  <div class="flex flex-wrap items-center gap-2">
    <l-tag
      class="criterion criterion-site"
      removable
    >
      <l-icon
        slot="prefix"
        name="mdi:map-marker-outline"
      ></l-icon>
      Lyon branch
    </l-tag>
    <l-tag
      class="criterion criterion-department"
      removable
    >
      <l-icon
        slot="prefix"
        name="mdi:sitemap-outline"
      ></l-icon>
      Facilities
    </l-tag>
    <l-tag
      class="criterion"
      removable
      >LEASE-2025-001</l-tag
    >
  </div>
</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-text-color sets the label, border, and checkbox accent at once, and the background is derived from it. Add --selected-background-color for a different fill, --selected-border-color for a different line, 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-text-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-color:var(--l-color-bg-fill-success-soft)] [--selected-text-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

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 ​

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 — a full-height segment at the end of the chip.

CSS custom properties ​

--text-colorCustom property
Label color.
--background-colorCustom property
Chip fill.
--border-colorCustom property
Chip border, and the rule that splits off the remove button. Defaults to a tint of --text-color; setting it pins the line in every state, so a token-based theme keeps its own value on hover.
--selected-text-colorCustom property
Label, border, and checkbox accent when selected. Defaults to the library's form-control accent, lightened in dark mode.
--selected-background-colorCustom property
Chip fill when selected. Defaults to a tint of --selected-text-color.
--selected-border-colorCustom property
Chip border when selected. Defaults to a 45% tint of --selected-text-color. Selection replaces the line the same way it replaces the fill and the ink, so a chip themed with --border-color still gets its emphasis step; set this to theme that step too.
--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.
--border-radiusCustom property
Corner radius. Defaults to a full pill.