<!-- GENERATED by scripts/build-llms.mjs from llms/overlays.md — do not edit this file. -->

# `lr-tag`

- **Import** `import '@aceshooting/lyra-ui/components/lr-tag.js';` (stable tag alias; registers the tag)
- **Class** `LyraTag`, also available unregistered from `@aceshooting/lyra-ui/components/overlays/badge/tag.class.js`
- **Family** `components/overlays/` — see `llms/index.md` for its siblings
- **Status** `stable` since `4.0.0` — see the maturity and deprecation policy in `llms/shared.md`
- **Release history** [CHANGELOG.md](../../CHANGELOG.md); family-wide breaking-change summaries: [llms-full.txt](../../llms-full.txt)
- **Deprecations** none
- **Optional peers** none
- **Themeable via** 7 parts, 24 custom properties — see this component's own `@csspart`/`@cssprop` list below
- **Documented with** `lr-badge` (same section below)
- **Library-wide behavior** (events, form association, `locale`/`strings`, tokens, TS types): `llms/shared.md`

---

## `lr-badge` and `lr-tag`

Compact status labels. `LyraTag` extends `LyraBadge`, so the two share one visual contract; `lr-tag`
adds tag semantics and an optional remove affordance. Both are static text by default, matching the
mirrored upstreams and avoiding repeated announcements when a category badge is re-rendered.
Authors can opt a genuinely changing badge into live semantics with `role="status"`; that and every
other author-supplied role remain authoritative across updates, hydration, and reconnect.

**Visual break in 8.0.0 — a badge is no longer a pill by default.** Both components used to render
fully-rounded ends unconditionally. `--lr-badge-radius` now defaults to `var(--lr-radius)` (a rounded
rectangle) and the pill treatment moved behind the new opt-in `pill` boolean. Existing markup keeps
its corner radius only if you add `pill`, or set `--lr-badge-radius: var(--lr-radius-pill)` once at
the app level.

**Properties** (all are declared by `lr-badge` and inherited by `lr-tag`; `lr-tag` adds the
`variant="text"` spelling plus `withRemove` / `removable`):

- `variant: 'neutral' | 'brand' | 'primary' | 'success' | 'warning' | 'danger' = 'neutral'`
  (reflected) — the semantic palette. `primary` renders through the same brand palette while
  remaining `primary` on property reads, selectors, serialization and reflection. `lr-tag`
  additionally accepts and preserves `text`, rendering the neutral plain treatment.
- `size: '2xs' | 'xs' | 's' | 'm' | 'l' | 'xl' | 'small' | 'medium' | 'large' = 'm'` (reflected) — the same visual-density scale
  `<lr-chip>` uses, for typography/padding/minimum block size; `m` preserves the original badge
  dimensions. Both short and long upstream spellings round-trip verbatim while resolving to the
  same private effective size for rendering.
- `appearance: 'accent' | 'filled' | 'outlined' | 'filled-outlined' | 'plain' = 'filled-outlined'`
  (reflected) — **new in 8.0.0.** The second visual axis: `variant` picks the palette, `appearance`
  decides how much of it lands on the fill, the border and the text. `filled-outlined` (the default)
  is quiet tint + loud border + loud text, i.e. exactly the pre-8.0.0 treatment; `filled` drops the
  border, `outlined` drops the fill, `accent` fills solidly with on-loud text, and `plain` drops both
  fill and border while keeping the label color. The border-less appearances use a `transparent`
  border rather than `none`, so switching appearance never changes the badge's layout box.
- `pill: boolean = false` (reflected) — **new in 8.0.0.** Fully-rounded ends instead of the default
  rounded rectangle; see the visual break above. Since it defaults to `false`, `pill="false"` is not
  a way to switch it off — remove the attribute, or assign `.pill = false`.
- `attention: 'none' | 'pulse' | 'bounce' = 'none'` (reflected) — **new in 8.0.0.** An opt-in,
  infinitely-looping attention animation for a badge that has to be noticed: `pulse` draws an
  expanding ring, `bounce` hops the surface vertically (block-direction, so it needs no RTL
  mirroring). Both stop outright — not merely shorten — under `prefers-reduced-motion: reduce`.
  The default does not materialize an attribute; an explicit `attention="none"` is authoritative
  and suppresses the `pulse` shorthand.
- `pulse: boolean = false` (reflected) — upstream-compatible shorthand for the pulse attention
  treatment while the `attention` attribute is omitted. Any explicit attention value wins. Lyra's
  intentional `variant="neutral"` and `appearance="filled-outlined"` defaults remain unchanged
  because the two pinned upstreams disagree on both defaults.
- `withRemove: boolean = false` (attribute `with-remove`, reflected) — **`lr-tag` only, new in
  8.0.0.** Renders the remove affordance. `lr-badge` never renders one, even if the attribute is
  present on the markup.
- `removable: boolean = false` (attribute `removable`) — **`lr-tag` only.** Shoelace-compatible
  alias for `withRemove`; reading either property reports the shared state. Either authored
  attribute keeps removal enabled until both are absent. Assigning `false` through either property
  clears both attributes, while assigning `true` reflects that property's own spelling.

**Events:** `lr-remove` — noncancelable, no detail, bubbles and composes. Emitted by `lr-tag` only (a
badge emits nothing at all) when the remove button is activated by click or by Enter/Space while
focused; it is a real native `<button>`, so both come for free. Only rendered, and therefore only
fired, while `withRemove` / `removable` is set, and the event's `target` is the tag itself.

Like `<lr-chip>`, a removable `lr-tag` is controlled: activation only announces the request. The
tag remains connected even if a listener calls `preventDefault()` (the event is not cancelable),
and the consumer removes it by updating the collection that rendered it.

If that controlled listener synchronously removes the focused tag, or a direct property write
removes its action, focus moves to the nearest available composed action. Focus explicitly moved by
the listener is preserved.

For a removable tag, `focus(options?)`, `blur()`, and `click()` delegate to its native remove
button. A plain tag's `click()` retains native `HTMLElement.click()` behavior and dispatches one
host `click`; its `focus()` has no delegated target. Setting `withRemove` / `removable` false
revokes remove-button focus and activation synchronously before the outgoing button is rerendered,
so same-task `click()` uses the plain host behavior instead of firing `lr-remove`. `blur()` remains
available during that same-task window so it can release the stale focused button; after render
there is no owner to blur.

**Slots:** default (the label), `start` (content before the label, typically an icon) and `end`
(content after it) — both new in 8.0.0. Each wrapper collapses entirely (`display: none`, so no
stray gap) while its slot is empty, and is seeded from the light-DOM children before the first
render so declarative content never flashes hidden for a frame. Mark purely decorative slotted
content `aria-hidden`.

**CSS parts:** `base` and `badge` are aliases on the same badge/tag surface; `start` and `end` (the slot wrappers, hidden entirely
while empty), `content` (the wrapper around the default slot — this is the part that truncates with
an ellipsis, deliberately not `base`, so the tag's oversized remove hit target can overhang the
compact surface without being clipped), and `remove-button` (`lr-tag` only, rendered only while
`withRemove` / `removable`). The button also carries Shoelace's `remove-button__base` alias, so
either part name styles the same native button.

**Themeable custom properties.** Three layers, so a consumer can retune one without restating the
others. Public hooks are consumed through private use-site defaults instead of being redeclared on
the host, so values set directly or inherited from a theme ancestor both win. The same contract
reaches `lr-tag` unchanged.

_Overrides_ — undeclared by default, so they still inherit from a consumer's own ancestor rule, and
win over whatever `variant`/`appearance` resolved: `--lr-badge-background` (falls back to
`--lr-badge-fill`), `--lr-badge-border` (falls back to `--lr-badge-stroke`), `--lr-badge-color`
(falls back to `--lr-badge-text`).

_Palette — what `variant` chooses_ (new in 8.0.0): `--lr-badge-tint` (private default
`var(--lr-color-surface)`, the quiet fill; each non-neutral variant changes that private default to
`var(--lr-color-fill-quiet)`, which the shared variants sheet has already re-pointed at that
variant's row of the semantic grid), `--lr-badge-solid` (private default
`var(--lr-color-fill-loud)`, the loud fill used by `appearance="accent"`), `--lr-badge-edge`
(private default `var(--lr-color-border)`, the border color), `--lr-badge-ink` (private default
`var(--lr-color-text)`, the text color) and
`--lr-badge-on-solid` (default `var(--lr-color-on-loud)`, the text color that stays legible on
`--lr-badge-solid`). An inherited or direct public palette value remains authoritative. Neutral is
the only variant whose border and text colors differ, which is why `-edge` and `-ink` are separate
slots rather than one loud color.

_Surface — what `appearance` routes onto the box_ (new in 8.0.0): `--lr-badge-fill` (default
`var(--lr-badge-tint)`), `--lr-badge-stroke` (default `var(--lr-badge-edge)`) and `--lr-badge-text`
(default `var(--lr-badge-ink)`). Set one of these to retune a single appearance without touching the
palette.

_Density and shape:_ `--lr-badge-font-size` (default `var(--lr-font-size-sm)`),
`--lr-badge-padding-inline` (default `var(--lr-space-s)`) and `--lr-badge-min-height` (default
`var(--lr-size-1-25rem)`) — the trio each private effective-size rule rewrites to that step's font size,
inline padding and minimum block size; the `m` defaults above exactly reproduce the pre-`size` fixed
badge treatment. `--lr-badge-gap` (default `var(--lr-space-2xs)`, new in 8.0.0) is the space between
the `start` slot, the label and the `end` slot — it collapses on its own when a wrapper is empty,
because the empty wrapper is `display: none` rather than zero-width. `--lr-badge-radius` (default
`var(--lr-radius)`; `pill` raises it to `var(--lr-radius-pill)`) is `[part='base']`'s corner radius,
retunable without a `::part(base)` rule and, unlike the density trio, does not vary by `size` — the
same `--lr-button-radius` pattern.

_Attention_ (all new in 8.0.0): `--lr-badge-attention-duration` (default
`var(--lr-duration-ambient)` — one cycle of the animation), `--lr-badge-attention-easing` (default
`var(--lr-easing-emphasized)` — kept a separate token from the duration so the `animation` shorthand
expands to exactly one timing function), `--lr-badge-pulse-color` (default
`color-mix(in srgb, currentColor 40%, transparent)` — the expanding ring's color; upstream alias
`--pulse-color`),
`--lr-badge-pulse-spread` (default `var(--lr-size-0-25rem)` — how far the ring expands) and
`--lr-badge-bounce-distance` (default `var(--lr-size-0-1875rem)` — the hop's peak travel).

_`lr-tag`'s own two_ (new in 8.0.0): `--lr-tag-remove-radius` (default `var(--lr-badge-radius)`, so
retuning the tag's corner retunes the remove button's with it) and
`--lr-tag-remove-hover-background` (default `color-mix(in srgb, currentColor 16%, transparent)` —
the remove button's `:hover` fill).

**Known gotchas:**

- The remove button's hit target meets the shared `--lr-icon-button-size` minimum in both axes while
  the visible glyph stays compact. Its full allocation participates in layout with no negative
  margins, so adjacent compact tags retain disjoint targets.
- Its accessible name is computed from the default slot's own text ("Remove {label}", localized;
  bare "Remove" for a label-less tag) and re-derived live when that text changes. Text inside the
  decorative `start`/`end` slots never leaks into it. Visible accessible text, forwarding-slot
  reassignment and external assigned-node mutations stay synchronized; hidden/inert/CSS-hidden/
  `aria-hidden` branches are excluded. A host `aria-label`, including an explicit empty value,
  names a single aggregate `role="group"`; it is not copied to the nested action. The remove button
  retains its purpose-specific visible-label/localized-fallback name.
- `appearance` and `variant` are orthogonal: `appearance="plain"` on `variant="danger"` still reads
  as danger, because the palette is chosen before the surface routing.

```html
<lr-badge variant="success" appearance="accent" pill size="s">
  <svg slot="start" aria-hidden="true" width="12" height="12">
    <!-- icon -->
  </svg>
  Live
</lr-badge>

<lr-tag variant="brand" appearance="outlined" with-remove>Design</lr-tag>
<script type="module">
  import "@aceshooting/lyra-ui/components/overlays/badge/badge.js";
  import "@aceshooting/lyra-ui/components/overlays/badge/tag.js";

  document.querySelector("lr-tag").addEventListener("lr-remove", (e) => {
    e.target.remove(); // in an app, update the backing collection instead
  });
</script>
```
