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

# `lr-chip`

- **Import** `import '@aceshooting/lyra-ui/components/lr-chip.js';` (stable tag alias; registers the tag)
- **Class** `LyraChip`, also available unregistered from `@aceshooting/lyra-ui/components/overlays/chip/chip.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** 6 parts, 13 custom properties — see this component's own `@csspart`/`@cssprop` list below
- **Documented with** `lr-chip-group` (same section below)
- **Library-wide behavior** (events, form association, `locale`/`strings`, tokens, TS types): `llms/shared.md`

---

## `lr-chip` / `lr-chip-group`

A small, content-agnostic surface for a short label: a tag, an active-filter/scope indicator, etc.
Distinct from `<lr-attachment-chip>` (specifically file-shaped, with a thumbnail/size/upload-
progress) — this pair carries no domain assumptions at all. `<lr-chip>` is a controlled component:
clicking its remove (×) button only fires `lr-remove` — the chip never removes itself from the DOM
on its own interaction, the same contract `<lr-attachment-chip>`/`<lr-conversation-item>`
already follow.

Collapsed groups reapply `max-visible` when assigned children are replaced or reordered at the same
count, preserving authored hidden/inert state and releasing departed visibility leases.

**Two breaks in 8.0.0.** `tone` is now `variant`, with no alias — one concept, one spelling,
library-wide. And a chip is **no longer a pill by default**: `--lr-chip-radius` used to be
`var(--lr-radius-pill)` unconditionally, is now `var(--lr-radius)` (a rounded rectangle), and the
fully-rounded treatment moved behind the new opt-in `pill` boolean. Existing markup keeps its corner
radius only if you add `pill`, or set `--lr-chip-radius: var(--lr-radius-pill)` once at the app
level. `<lr-badge>`/`<lr-tag>` made the identical shape change, with the identical `pill` opt-in.

**Two breaks in 9.0.0.** The chip's leading adornment slot and CSS part are now `start`, matching
the library-wide adornment vocabulary; migrate `slot="icon"` to `slot="start"` and
`::part(icon)` to `::part(start)`. Also, `toggleable` is now the sole toggle-mode opt-in:
`selected` represents only current pressed state, so add `toggleable` anywhere that previously
relied on `<lr-chip selected>` to create an action.

### `lr-chip`

**Properties:**

- `size: '3xs' | '2xs' | 'xs' | 's' | 'm' | 'l' | 'xl' | 'small' | 'medium' | 'large' = 'm'`
  (reflected) — standard visual-density scale for typography, padding, gap, and icon size; `m`
  preserves the original chip dimensions. The `small`/`medium`/`large` spellings are exact aliases
  of `s`/`m`/`l` and round-trip unchanged. Unsupported attributes and untyped property writes
  normalize to reflected `m`.
- `variant: 'neutral' | 'brand' | 'success' | 'warning' | 'danger' = 'neutral'` (reflected) —
  **renamed from `tone` in 8.0.0, with no alias** (see above). `<lr-badge>`, `<lr-callout>` and
  `<lr-toast-item>` all already spelled it `variant`. It tints the whole surface using the
  loud-color-on-quiet-tint convention: background is the
  variant's quiet fill, text/icon its loud fill, both read from the shared semantic grid. `neutral`
  deliberately opts out of that grid and falls back to a plain bordered-surface look. Unsupported
  attributes and untyped property writes normalize to reflected `neutral`.
- `removable: boolean = false` (reflected — shows the remove (×) button)
- `disabled: boolean = false` (reflected) — disables the active native toggle/remove control,
  blocks focus and activation, and suppresses selection/removal requests without mutating state
- `pill: boolean = false` (reflected) — **new in 8.0.0.** Fully-rounded ends instead of the default
  rounded rectangle; the same property `<lr-badge>`/`<lr-tag>` carry. Since it defaults to `false`,
  `pill="false"` is not a way to switch it off — remove the attribute, or assign `.pill = false`.
- `selected: boolean = false` (reflected) — current pressed value. It does not opt into interaction
  or selected styling by itself; set `toggleable` independently. Once toggle mode is active, a
  separate native `[part='toggle-button']` owns focus, Enter/Space/click
  activation, and explicit `"true"`/`"false"` `aria-pressed`; `[part='base']` remains a container
  and the visible default-slot label is inert and aria-hidden. Activation proposes the opposite
  value through the cancelable `lr-chip-select` event and mutates `selected` only when that event is
  not prevented.
  Has no toggle effect when combined with `removable`, where the remove button is the sole control.
- `toggleable: boolean = false` (reflected) — sole opt-in into the toggle/pressed interactive mode,
  independent of `selected`'s current value. Pair it with `selected` for an initially pressed chip;
  leave `selected` unset for an initially unpressed chip.
- `value?: string` — opaque consumer bookkeeping value, never read, validated, or rendered by this
  component itself, only ever echoed back verbatim (including `undefined` if never set) in
  `lr-remove`'s detail
- `wrap: boolean = false` (reflected) — wraps `[part='label']` onto multiple lines instead of
  ellipsis-truncating it to one line. `false` (the default) reproduces the chip's original
  single-line, ellipsis-truncated label exactly. Matches `<lr-suggestion-chips>`'s identical `wrap`
  opt-in.

**Events:** `lr-remove` (`detail: { value }` — the remove (×) button was activated via click or
Enter/Space while focused; only rendered/reachable while `removable`), `lr-chip-select`
(`detail: { value, selected }` — cancelable; fired from the native toggle button on click or
Enter/Space with the proposed next state when toggle mode is active and `removable` is not set.
Calling `preventDefault()` keeps the current `selected` state unchanged)

**Methods:** `focus(options?)`, `blur()`, and `click()` forward to the active internal control
(toggle or remove button); a disabled control refuses focus/click, and a passive chip's `click()`
retains ordinary host behavior.

When `removable`, `toggleable`, or `disabled` replaces a focused control, focus follows to the
equivalent new chip control when one exists, otherwise to the nearest available composed action.
Synchronous controlled removal receives the same repair, and a newer external focus destination is
never overridden.

**Slots:** default (the chip's label content; its flattened subtree is inert and aria-hidden in
toggle mode, so move links/buttons outside a toggleable chip), `start` (optional decorative leading
adornment such as an icon or status dot; its flattened subtree stays visible but is always inert
and aria-hidden, and nothing is reserved for it — no extra gap — when left empty), `end` (optional trailing content,
typically an icon, placed after the label and before the toggle/remove button; nothing is reserved
for it — no extra gap — when left empty, mirroring `<lr-badge>`'s identical `end` slot). `end`
remains ordinary consumer content in passive/removable mode, but its flattened subtree becomes
inert and aria-hidden beneath the full-surface toggle.

Toggle/remove action names follow the default slot's live visible accessible text through nested
forwarding slots and assigned-node replacement; decorative `start` content never leaks into them.
Hidden, inert, CSS-hidden and `aria-hidden` label branches are excluded. When a host `aria-label`
is present—including `aria-label=""`—the host becomes the one aggregate `role="group"` owner;
that label is not copied onto the nested action. The toggle/remove button instead keeps its
purpose-specific name from visible label text or the localized `select`/`remove` fallback, so the
host and action never expose duplicate names.

**CSS parts:** `base` (the pill's root container), `start` (inert, aria-hidden wrapper around the
decorative `start` slot; hidden entirely while empty), `label` (wrapper around the default slot,
inert and aria-hidden in toggle mode), `end` (wrapper around the `end` slot; hidden entirely while
empty and inert plus aria-hidden in toggle mode, the same `end` csspart name `<lr-badge>` uses),
`toggle-button` (the real native toggle control, rendered over the label in toggle mode),
`remove-button` (the remove (×) affordance, only rendered while `removable`)

The toggle control's accessible name comes from the chip's default-slot text. A start-only
toggleable chip (a colour swatch standing in for a chart series, a bare status dot) falls back to
the localized `select` message rather than shipping an unnamed focusable button—the same generic
fallback the remove button makes to `remove`. A host name, when supplied, remains on the aggregate
group as described above.

**Themeable custom properties:** `--lr-chip-accent`, `--lr-chip-bg`, `--lr-chip-border`
(component-local trio whose private defaults change per `variant` rather than repeating
background/color/border per part per variant; default `var(--lr-color-text)` /
`var(--lr-color-surface)` / `var(--lr-color-border)` —
mirrors the same accent/bg/border vocabulary `<lr-tool-call-chip>`/`<lr-attachment-chip>` use. One
rule covers all four non-neutral variants, because the shared variants sheet has already re-pointed
`--lr-color-fill-loud`/`--lr-color-fill-quiet` at the active variant's row of the semantic grid —
the chip reads those generic slots and never names a variant, and sets its border `transparent`),
`--lr-chip-pressed-border` (border color while pressed/selected — falls back to
`--lr-chip-accent`), `--lr-chip-pressed-bg` (background color while pressed/selected — falls
back to `--lr-chip-bg`), the density quintet `--lr-chip-font-size`, `--lr-chip-padding-block`,
`--lr-chip-padding-inline`, `--lr-chip-gap`, `--lr-chip-icon-size` (all five have private defaults
that follow each `size`, so setting one on the element or a theme ancestor remains authoritative; the
`m` defaults are `--lr-font-size-sm` / `--lr-size-0-25rem` / `--lr-space-s` / `--lr-space-xs` /
`--lr-font-size-sm`), the height pair `--lr-chip-min-height` / `--lr-chip-height` (below),
`--lr-chip-radius` (default `var(--lr-radius)`; `pill` changes its private default to
`var(--lr-radius-pill)`) — the
corner radius of both `[part='base']` and `[part='remove-button']`, kept in sync so retuning one
retunes both, retunable without a `::part()` rule, and unlike the density quintet above it does not
vary by `size`; the same `--lr-button-radius` pattern —
plus shared tokens (`--lr-space-xs`, `--lr-space-s`,
`--lr-color-fill-loud`/`-fill-quiet`, `--lr-color-surface`, `--lr-color-border`, `--lr-color-text`,
`--lr-color-mix-active`,
`--lr-icon-button-size`, `--lr-focus-ring-width`, `--lr-focus-ring-color`,
`--lr-focus-ring-offset`, `--lr-transition-fast`).

**Chip height — a floor and an exact cap:**

- `--lr-chip-min-height` (default `--lr-size-1-5rem`) controls the component-density floor for
  **every interactive chip**—toggleable and removable alike. The final used block size is also
  floored by the shared `--lr-icon-button-size` target in both modes, while `l`/`xl` raise the
  density default. A passive display chip takes no floor from this at all. The interactive base is
  likewise allocated at least the shared target width, so its absolute toggle action cannot escape
  into an adjacent control.
- `--lr-chip-height` pins an **exact** height on `[part='base']` — interactive and passive chips
  alike — so a row of chips can line up with a sibling control of a known height. It is
  **undeclared by default**, which is what keeps the per-tier floor alive: `auto` is a valid
  declared value that would win over the `var()` fallback arm and make `--lr-chip-min-height` dead
  code, so never set it to `auto` — remove the declaration instead. Because the component never
  declares it, it can be set inline, from an ancestor, or from an outer-tree rule.
  On an interactive chip, a value below the shared target size controls only the painted density;
  the owned action allocation still expands to the target floor and cannot overlap adjacent
  controls.

**Optional peer deps:** none.

```html
<lr-chip
  toggleable
  selected
  style="--lr-chip-bg: var(--lr-color-surface); --lr-chip-pressed-bg: var(--lr-color-warning-quiet)"
>
  Priority filter
</lr-chip>
```

### `lr-chip-group`

A flex-wrap container for a set of `<lr-chip>` children — plain light-DOM composition, direct
children are the chips (the same shape `<lr-multi-split>`'s panels / `<lr-source-list>`'s cards take,
no `.items` array prop).

**Properties:**

- `accessibleLabel: string = ''` (attribute `aria-label`) — accessible name for the group,
  forwarded to `[part='base']`, which carries `role='group'` whenever a name is supplied. Supply
  one for any chip row that means something as a set (a filter row, a tag list); without it the
  container stays roleless, because an unnamed group role adds verbosity without information.
  A host `aria-label` does **not** reach a shadow root on its own — this property is what makes it
  reach `[part='base']`, the element that owns the role. Mirrors how `lr-radio-group` and
  `lr-segmented` name their own `radiogroup`.
- `maxVisible?: number` (attribute `max-visible`) — maximum number of assigned children shown before
  the rest collapse behind a "+N" indicator; flattened slot-forwarded children count the same as
  direct children. Author-hidden or inert children do not consume capacity or inflate the hidden
  count. Unset means no limit.

**Events:** `lr-overflow-toggle` (`detail: { expanded }` — the overflow indicator was activated,
revealing or re-collapsing the excess children; fires only from that click, i.e. only when
`max-visible` is actually causing an overflow state — never as a side effect of `max-visible`/
children changing on their own)

When collapse, `max-visible`, or controlled child removal hides the focused chip action, focus moves
to the nearest enabled visible chip control, then the overflow disclosure, then the stable group
base. Focus already moved outside the group is preserved.

**Slots:** default (`<lr-chip>` elements, or any content, though the chip pairing is the intended
usage)

**CSS parts:** `base` (the flex-wrap container, holds both the slot and the overflow indicator),
`overflow-indicator` (the "+N" / "Show less" toggle button; only rendered while `max-visible` is
actively causing an overflow—a locally-styled pill, not an instantiated real `<lr-chip>`, with the
shared minimum hit area in both axes)

**Themeable custom properties:** `--lr-chip-group-gap` (default `var(--lr-space-xs)`) — the gap
between `[part='base']`'s wrapped children (chips and the overflow indicator alike); previously
hardcoded. `--lr-chip-group-overflow-expanded-color` (default
`var(--lr-color-text)`) — text color of `[part="overflow-indicator"]` while expanded
(`aria-expanded="true"`). `--lr-chip-group-overflow-expanded-border-style` (default `solid`) —
that same expanded indicator's border style; its resting border deliberately remains dashed, so a
theme can retune the open affordance without losing the collapsed treatment. Both are state hooks:
inline `var()` fallbacks at the point of use, never `:host` declarations, so they can be set on the
element _or on any ancestor_. They exist because
`::part(overflow-indicator)[aria-expanded='true']` is invalid CSS — Shadow Parts forbids an attribute
selector after `::part()` — so retinting or reshaping only the expanded state otherwise meant
re-pointing shared tokens. Left unset, rendering is unchanged. Otherwise shared tokens
(`--lr-space-xs`, `--lr-space-s`,
`--lr-color-border`, `--lr-color-surface`, `--lr-color-text-quiet`, `--lr-color-text`,
`--lr-color-brand`, `--lr-focus-ring-width`, `--lr-focus-ring-color`,
`--lr-focus-ring-offset`, `--lr-transition-fast`).

**Optional peer deps:** none.

```html
<lr-chip-group max-visible="3">
  <lr-chip removable value="draft">Draft</lr-chip>
  <lr-chip variant="success" removable value="reviewed">Reviewed</lr-chip>
  <lr-chip variant="warning">Needs input</lr-chip>
  <lr-chip variant="danger" pill>Blocked</lr-chip>
</lr-chip-group>
<script type="module">
  const group = document.querySelector("lr-chip-group");
  group.addEventListener("lr-overflow-toggle", (e) =>
    console.log(e.detail.expanded)
  );
  group
    .querySelectorAll("lr-chip")
    .forEach((chip) =>
      chip.addEventListener("lr-remove", (e) => console.log(e.detail.value))
    );
</script>
```

Since CSS alone can't parameterize `:nth-child` on a runtime prop, `<lr-chip-group>` reaches
directly into the light DOM and sets each excess child's own `hidden` property once `max-visible` is
exceeded — the same approach `<lr-multi-split>` uses to set each panel's inline `flex`/`order`, rather
than a stylesheet-only solution. It observes live author changes to each managed child's `hidden`
and `inert` state, uses real `hidden` attributes for arbitrary HTML/SVG elements, and restores the
latest author-owned value when ownership ends or the group disconnects; reconnecting reapplies the
current collapsed state. Forwarded-slot reconciliation is deferred without scheduling a reactive
write from `firstUpdated()`.

**Known gotchas:**

- `<lr-chip>`'s accessible remove-button label ("Remove {text}") is computed only from the default
  slot's own text content — text living inside the (decorative) `start` slot doesn't leak into it.
- `<lr-chip-group>` silently un-expands (`expanded` resets to `false`, with no event firing) if a
  consumer raises `max-visible` past the current child count while already expanded — only an actual
  click on the overflow indicator fires `lr-overflow-toggle`.
- `<lr-chip-group>`'s overflow indicator is its own locally-styled pill, not an instantiated
  `<lr-chip>` in its shadow DOM — don't expect `<lr-chip>`'s CSS parts or custom properties to
  reach it.

---
