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

# `lr-icon-button`

- **Import** `import '@aceshooting/lyra-ui/components/lr-icon-button.js';` (stable tag alias; registers the tag)
- **Class** `LyraIconButton`, also available unregistered from `@aceshooting/lyra-ui/components/forms/icon-button/icon-button.class.js`
- **Family** `components/forms/` — 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** `dompurify` — see `llms/peers.md`
- **Themeable via** 3 parts, 11 custom properties — see this component's own `@csspart`/`@cssprop` list below
- **Library-wide behavior** (events, form association, `locale`/`strings`, tokens, TS types): `llms/shared.md`

---

## `lr-icon-button`

An accessible icon-only action/link with a native `<button>` inside. It is deliberately not a
form-associated submitter; use `<lr-button circle type="submit|reset">` with an icon-only default
slot when a form action is required. Because it is not form-associated, an ancestor
`<fieldset disabled>` does not disable it and it is absent from `form.elements`; set `disabled`
directly on each icon button.

External descriptions follow the current source element identity when an element with the same ID
replaces it, when a source is removed or reinserted, and when the control reconnects or moves to
another document. Host-root lookup and ordering follow the current `aria-describedby` list,
including unresolved IDs and duplicates. Switching between native button and anchor modes keeps the
relationship on the current action. Existing accessible names and `aria-controls` relationships
retain their separate contracts.

Its public `--lr-icon-button-*` theme inputs stay undeclared on the host, so an ancestor theme
wrapper can override the built-in fallbacks; a value set directly on the element still wins.

Host `aria-labelledby` IDREFs are resolved onto the internal control through
`ariaLabelledByElements`, so a composing component can name the button from elements in its own
shadow root — an idref cannot cross that boundary, the reflected element reference can. Per ARIA it
wins over `aria-label`/`label` and the localized fallback name.

**Lean registration entry.** `components/forms/icon-button/icon-button.js` eagerly imports
`<lr-icon>` so an `icon`/`src` attribute paints synchronously. A consumer who only ever slots their
own SVG can import `components/forms/icon-button/icon-button-register.js` instead, which registers
`<lr-icon-button>` and nothing else — no `<lr-icon>` implementation in the entry chunk and no
unreachable sanitizer chunk. Setting `icon`/`src` on a button registered that way renders no glyph
until `<lr-icon>` is registered by something else.

**Properties:**

- `icon: string = ''` — an `lr-icon` glyph name (see `llms/components/lr-icon.md`)
- `name: string = ''` — Shoelace alias for `icon`; reads and writes stay synchronized. Assigning
  the upstream `undefined` spelling clears both names to the canonical `''` read value
- `library?: string` / `src?: string` — forwarded to the nested `lr-icon`, preserving Shoelace
  icon-library and remote-SVG markup. Remote SVG loading inherits `lr-icon`'s URL, byte-ceiling,
  sanitization, and stale-generation guards
- `accessibleLabel: string = ''` (attribute: `aria-label`) — the typed override for the button's
  accessible name; wins over `label`
- `label: string = ''` — accessible name when `accessibleLabel` is unset
- `disabled: boolean = false` (reflected)
- `href?: string`, `target?: string`, `download?: string` — a safe `href` renders a native anchor;
  `target` derives `rel="noopener noreferrer"`, and download presence (including `download=""`)
  selects the stricter downloadable-URL allowlist. A disabled link keeps the anchor but removes
  `href`

With neither `accessibleLabel` nor `label` set, the name falls back to the localized
`iconButtonLabel` string rather than being empty — override it per instance with `.strings` or
app-wide with `registerLyraLocale()` (see `llms/shared.md`); don't rely on the fallback for a
button whose purpose isn't generic.

Host `aria-haspopup` and `aria-expanded` values are forwarded reactively to the shadow-internal
native button. `aria-pressed` (`true`, `false`, `mixed`) supports icon-only toggle actions such as
mute, favorite, or pin, and reaches the `<button>` rendering only — a link icon-button (`href` set)
never receives it, since `role="link"` has no pressed state (that carve-out arrived in **16.0.0**;
earlier versions forwarded it onto the anchor too); `aria-current` (`page`, `step`,
`location`, `date`, `time`, `true`, `false`) is global and supports current-item icon buttons such
as an active nav/pagination target in both renderings. Both follow attribute
changes, removal, and button/link replacement without changing the native role of an enabled
control; an empty or unsupported token is omitted — mirroring `lr-button`'s own
`aria-pressed`/`aria-current` forwarding. A disabled link icon-button drops `href` and renders an
explicit `role="link"` in place of the implicit role that goes with it, keeping its forwarded
accessible name legal. Host `aria-describedby` targets in the host's own root are resolved through
`ariaDescribedByElements`. Host `aria-controls` targets use the corresponding
`ariaControlsElements` API, so using `<lr-icon-button slot="trigger">` inside `<lr-menu>` exposes
the menu relationship and expanded state on the element that actually receives focus. Supporting
browsers intentionally clear each serialized internal IDREF attribute after its explicit element
list is assigned. Browsers without those APIs retain the forwarded string attributes as
best-effort fallbacks.

**Methods:** `focus(options?)`, `blur()`, and `click()` forward to the native interactive root,
activating the action button or a safe anchor through the same path as pointer/keyboard input.
`getToolbarActions(): readonly LyraToolbarAction[]` contributes this control as one logical action
to `<lr-message-actions>` and any other `LyraToolbarAction` toolbar. A roving-tabindex owner needs
it: writing `tabindex` on a custom-element host neither adds nor removes its shadow-internal
button's tab stop, so without it a slotted icon button either stays permanently tabbable or drops
out of the toolbar's stop list. `control: HTMLButtonElement | HTMLAnchorElement | null` (read-only)
is the internal native control that owns the role, for projecting a host IDREF relationship onto it;
it is `null` before the first render, mirroring `<lr-virtual-list>`'s `scrollContainer`.

**Events:** a plain native `click` crosses the shadow boundary unmodified. The internal button's
`focus` and `blur` are re-dispatched from the host as bubbling, composed events.

**Slots:** (default) — custom icon content. It is rendered **beside** the `icon` glyph, as a
sibling of it, not piped through `<lr-icon>`: the internal `<lr-icon>` mounts only when `icon` is
set, so with `icon` left empty your content is the button's only child. That is what lets a
complete element — an `<svg>`, an `<img>`, an `<lr-flag>` — render at its own natural aspect ratio
instead of being forced into a 1:1 box. Setting both `icon` and slotted content renders both, side
by side; that is a valid composition, not a fallback.

**Bare SVG geometry fallback:** slotted bare SVG _geometry_ (`path`, `circle`, `rect`, `line`,
`polygon`, `polyline`, `ellipse`, `g`, `use`) with no `icon` set and no enclosing `<svg>` of its
own has no real SVG parent as parsed, and is detected and cloned into an internal
`[part="fallback"]` SVG-namespaced element so it still paints — the same fallback `<lr-icon>`'s own
custom-content slot uses. The `[part="fallback"]` svg carries the same `fill="none"
stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"` defaults
`<lr-icon>`'s own wrapper svg does, so bare stroke-style geometry (the common lucide/feather/tabler
path-data shape, which carries no fill/stroke of its own) renders outlined instead of as a solid
shape; an explicit `fill`/`stroke`/etc. already present on the slotted node still wins for that
node. This is narrowly scoped to that whitelist: a complete `<svg>`, `<img>`, or custom element
(e.g. `<lr-flag>`) is never touched by it and keeps rendering as an untouched sibling at its own
natural aspect ratio.

**CSS parts:** `base`/`button` (the same native button or anchor), `fallback` (only present in the
DOM while at least one top-level slotted element needs the bare-geometry fallback above)

**Themeable custom properties:** `--lr-icon-button-size` (default `2.5rem`) is the **minimum**
tappable inline and block size of the native button — a floor, not a fixed size. Content larger
than it grows the button and keeps its own aspect ratio; a small glyph pads out to it. It is a
library-wide token (declared on every `lr-*` host by the shared token layer, and the shared minimum tappable size
that several other components size their icon-only controls against), so overriding
`--lr-theme-icon-button-size` globally resizes all of them together. Keep the resolved value at or
above 24px — see `llms/shared.md`. **`--lr-icon-button-size` is element-scoped, unlike every other
token in this section:** the shared token layer re-declares it on every `lr-*` host's own `:host`,
so a rule that sets `--lr-icon-button-size` on an ancestor wrapper is reset the moment it crosses
into any intervening `lr-*` component and never reaches a `<lr-icon-button>` composed inside it
(e.g. one slotted through `<lr-popover>`). The three levers that actually reach it are: setting
`--lr-icon-button-size` directly on the icon button element itself, where no intervening component
sits between the rule and the property; setting `--lr-icon-button-size-scope` on any ancestor to
resize one subtree; or setting `--lr-theme-icon-button-size` on an ancestor to resize the whole
application. The shared layer reads the latter two through `var()` at every level, so both reach a
`<lr-icon-button>` composed inside another component, e.g. `<lr-copy-button>`/
`<lr-message-actions>` -- see `llms/shared.md` for the full scope table.

Lowering the floor for a dense action row below 24px is safe **only through the two ancestor
levers**. The coarse-pointer/no-hover media rule reads those, and floors the RENDERED hit area back
at 2.75rem/44px however far the override lowered it, so the control stays comfortably tappable the
moment the pointer reaching it is a finger rather than a mouse. It does **not** rescue
`--lr-icon-button-size` set directly on the element: that declaration comes from the outer tree and
outranks the shadow tree's own `:host` rule, so the media rule never wins and the rendered hit area
is exactly the value set -- `--lr-icon-button-size: 1rem` really does render a 16px target under a
coarse pointer, which fails WCAG 2.2 SC 2.5.8. Prefer `--lr-icon-button-size-scope` when the intent
is a denser row; reach for the element-scoped name only as a deliberate, localized trade-off. `--lr-icon-button-radius` (default
`--lr-radius`) is not re-declared anywhere in the shared layer, so — like every other token below —
it inherits normally from an ancestor even through an intervening component; it is the
`[part='button']` corner radius, retunable without a `::part(button)` rule — the same
`--lr-button-radius` pattern; `lr-icon-button` has no `size` tiers, so there is no per-tier gap
counterpart to it. The internal control sets `font: inherit`, so an `em`-sized slotted glyph takes
the surrounding text's font-size rather than the native button's UA default.

The rest come in resting/hover/pressed triples, each falling through to the next-quieter state so
setting only one still behaves:

- `--lr-icon-button-background` (default `transparent`),
  `--lr-icon-button-background-hover` (default `color-mix(in oklab, var(--lr-color-surface),
var(--lr-color-mix-partner) var(--lr-color-mix-hover))`) and
  `--lr-icon-button-background-active` (the same mix at the stronger `--lr-color-mix-active` share,
  so a press reads as more than a hover) — the `[part='button']` background in each state. The
  hover fallback used to be `--lr-color-surface` itself, i.e. the page background, so hovering an
  icon button on a default page changed nothing at all.
- `--lr-icon-button-color` (default `inherit`), `--lr-icon-button-color-hover` (default
  `var(--lr-icon-button-color, inherit)`) and `--lr-icon-button-color-active` (default
  `var(--lr-icon-button-color-hover, var(--lr-icon-button-color, inherit))`) — the icon/text colour.
- `--lr-icon-button-border` (default `0`), `--lr-icon-button-border-hover` (default
  `var(--lr-icon-button-border, 0)`) and `--lr-icon-button-border-active` (default
  `var(--lr-icon-button-border-hover, var(--lr-icon-button-border, 0))`) — the _complete_ native
  border shorthand, replaced wholesale in each state rather than merged.

These are the same per-component indirection `lr-button`'s
`--lr-button-fill`/`--lr-button-on-fill` provide, letting a single button be bordered and tinted
without a `::part(button)` rule. All nine are undeclared by default and read as inline `var()`
fallbacks, so setting only the resting value carries through hover and press, and setting none of
them leaves rendering unchanged.

```html
<lr-icon-button name="search" library="default" label="Search"></lr-icon-button>
<lr-icon-button
  name="chevron-right"
  label="Open documentation"
  href="https://example.com/docs"
  target="_blank"
></lr-icon-button>
```
