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

# `lr-emoji-picker`

- **Import** `import '@aceshooting/lyra-ui/components/lr-emoji-picker.js';` (stable tag alias; registers the tag)
- **Class** `LyraEmojiPicker`, also available unregistered from `@aceshooting/lyra-ui/components/forms/emoji-picker/emoji-picker.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** `emoji-picker-element-data` — see `llms/peers.md`
- **Themeable via** 17 parts, 27 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-emoji-picker`

A searchable, keyboard-navigable, form-associated emoji picker. `groups` is fully consumer-suppliable
— the component ships no emoji data of its own — in the same "zero/optional-peer dependency" spirit
as `<lr-lite-chart>`/`<lr-heatmap>`; an optional convenience auto-loader fetches a default set on
connect from the `emoji-picker-element-data` peer, but only when `groups` hasn't already been
supplied (an explicit empty array still counts as supplied and skips the auto-load).
When the filtered set reaches 200 items, the grid automatically windows its visible rows while
preserving the full option count through `aria-setsize`/`aria-posinset`.

Removing `label`, `hint`, or `error-text` treats the removed value as absent for rendering while
preserving native attribute-removal property readback. Host `aria-describedby` references resolve in
the host root onto the value-owning listbox before its local error/hint guidance. Target
replacement, removal, reinsertion, reconnect, and adoption keep that relationship current. The
search input keeps its separate description ownership.

`form.reset()` restores the default value and pristine interaction feedback. Required constraints
and persistent custom validity remain in force; a required-empty value remains invalid. Composing
key events (`isComposing` or legacy key code 229) remain with text editing and do not navigate or
pick an emoji.

Ships the same opt-in `label`/`hint`/`errorText` form-control chrome as `lr-select`/
`lr-color-picker` (props + matching named slots + `form-control`/`form-control-label`/`hint`/
`error` CSS parts) — left unset, none of that chrome renders.

Public `--lr-emoji-picker-*` theme inputs stay undeclared on the host, so an ancestor theme wrapper
can override size-tier fallbacks; a value set directly on the element still wins.

**Properties:** the shared form properties `name`, `value`, `defaultValue`, `customError`
(`custom-error`), `disabled`, and `required`, plus
`groups: readonly EmojiPickerGroup[] = []` (attribute: false) — readonly `EmojiPickerGroup { key,
label, emojis: readonly EmojiPickerItem[] }`, readonly `EmojiPickerItem { emoji, name,
shortcodes? }`; assignment captures a bounded frozen owned snapshot, including the current contents
of reused source item objects. Earlier snapshots remain frozen and unchanged; in-place source edits
become visible only after an explicit `groups` assignment. The search field matches
`name` and every `shortcodes` entry, case-insensitively. Consumer group labels render verbatim.
Groups returned by the built-in loader carry private provenance, letting their fixed emojibase
headings follow `registerLyraLocale()`/`.strings` through filtering and windowed rendering, including
same-locale `.strings` changes, without exposing localization keys as consumer data. Caller-authored
headings remain literal even when their keys match built-in groups. Empty (the default, before the auto-loader
resolves) renders just the search input and the empty state. `accessibleLabel` (`aria-label`)
forwards a host-supplied accessible name to the internal `role="listbox"` grid. Only omission falls
back to the visible label or localized default; an explicit `aria-label=""` remains empty.
`label: string = ''` — visible label rendered above the
search/grid; unset renders no label chrome. When `label` (or the `label` slot) is set and
`accessibleLabel`/a host `aria-label` is not, the grid's accessible name switches from the
localized default to `aria-labelledby` pointing at the visible label. `hint: string = ''` —
supporting text rendered below the search/grid; unset renders no hint chrome. `errorText: string =
''` (attribute `error-text`) — validation-error text rendered below the hint (overridden by slotted
`error` content when provided); unset renders no error chrome. `size: '2xs' | 'xs' | 's' | 'm' | 'l' | 'xl' = 'm'` —
visual size; scales the glyph and preferred emoji box while every interactive option remains
floored at the shared `--lr-icon-button-size`.

**Methods:** `focus(options?)`, `blur()`, and `click()` delegate to the search input/current owned
focus target, plus `getForm()`, `checkValidity()`, `reportValidity()`, `setCustomValidity(message)`, and
`resetValidity()` provide the shared form-validation surface. `resetValidity()` clears only
consumer-supplied custom validity and recomputes current intrinsic constraints; it does not change
`value`/`defaultValue`, clear prior interaction state, or force a required-empty picker valid.

**Events:** a pick emits native `InputEvent` `input`, `lr-input`, native `Event` `change`, then
`lr-change`; both aliases carry `detail: { value }`. The internal search input's `focus` and `blur`
are relayed once as native `FocusEvent`s preserving `relatedTarget`.
All four native events use the picker's current owner-document realm, including
after adoption. `lr-invalid` (no detail) is emitted once as a cancelable alias when native validity
fails; preventing it also prevents the native `invalid` event that produced it. Programmatic `value`
changes are silent.

**Keyboard:** the grid is a roving-tabindex listbox (a single Tab stop — only the active emoji is
tabbable). ArrowLeft/ArrowRight step the active item backward/forward following reading direction
(swapped under RTL), ArrowUp/ArrowDown move by one visual row (measured from the live wrap layout),
Home/End jump to the first/last item, and Enter/Space picks the active item. The search input is a
`role="combobox"` over the same listbox: the arrow keys and Enter also work while focus stays in
the input, with `aria-activedescendant` tracking the active option. Hovering an emoji with the
pointer also moves the active item to it. When a controlled `groups` replacement removes the
focused option, focus moves to the nearest surviving option; when the same item object remains,
its identity wins even if it moved. A replacement never pulls focus away from the search field or
an external control. In a windowed grid, roving navigation materializes an off-window target before
transferring focus, so End and long row jumps never strand focus on a removed virtual row.

**Slots:** `label` (custom label content), `hint` (custom hint content), `error` (custom error
content, overrides the `errorText` attribute when provided).

**CSS parts:** `form-control` (the outer wrapper around label, `base`, error and hint),
`form-control-label` (the visible label), `base`, `search-wrapper` (the row wrapper around
`search` and `search-clear`), `search` (`role="combobox"`), `search-clear` (clears the search
field, replacing the native search-cancel glyph the component resets; rendered only while it has a
value), `grid`
(`role="listbox"`, the scroll viewport), `group-label`, `emoji` (each emoji's own `role="option"`
button), `empty` (shown when the search matches nothing, or when a consumer deliberately opted out
with `groups = []`), `load-error` (the failure surface shown in `empty`'s place when the optional
peer failed to load), `hint` (the hint message), `error` (the
error message). The grid scrolls in the block axis and explicitly clips inline overflow, so an
allocation narrower than one option does not introduce a second scrollbar. While windowing is
active the rows are wrapped in `virtual-spacer`
(full-height scroll spacer), `virtual-row` (one absolutely-positioned row), `virtual-label` (an
`aria-hidden` spacer standing in for a row's missing `group-label`), and `virtual-items` (the row's
emoji flex line).

**The required marker.** `required` with a non-empty `label` paints the library's shared marker on
`[part="form-control-label"]` — the one `::after` rule described under "The required-field marker"
above, not a copy of it, so `--lr-form-control-required-content`,
`--lr-form-control-required-color` and `--lr-form-control-required-offset` retune or suppress it
here exactly as they do on `lr-input`. With no label text the part is hidden and no glyph is
painted.

**Themeable custom properties:** `--lr-emoji-picker-item-size` (default `--lr-icon-button-size`,
each emoji button's box; scaled by the `size` property), `--lr-emoji-picker-glyph-size` (default
`--lr-font-size-lg`, the font size of the emoji glyph; scaled by the `size` property to keep the glyph
proportional to the item box), `--lr-emoji-picker-gap` (default `--lr-space-2xs`, the gap between
emoji within a windowed row), and `--lr-emoji-picker-row-height` (default
`calc(var(--lr-emoji-picker-item-size) + var(--lr-space-l))`, one windowed row's height).
`--lr-emoji-picker-item-size`, `--lr-emoji-picker-gap`, and `--lr-emoji-picker-row-height` are also
read back in JS to derive columns-per-row and row offsets for the windowed layout,
resolved to real pixels by measuring hidden probe boxes the component's own stylesheet sizes from
those same tokens — so any CSS length unit works, `rem`/`em` and `calc()` included, and the windowed
geometry matches what is painted without expressing the tokens in `px`. The measurement is cached
and re-derived only when the resolved pixels can actually change (a token override applied after the
first render, a theme swap, a root or host font-size change feeding a `rem`/`em` value), never per
frame. `--lr-emoji-picker-search-min-height` (default `auto`),
`--lr-emoji-picker-search-font-size` (default `inherit`),
`--lr-emoji-picker-search-padding-inline` (default `var(--lr-space-s)`) and
`--lr-emoji-picker-search-padding-block` (default `var(--lr-space-xs)`) size the built-in filter
field. `size` does NOT drive them — on this component `size` scales the emoji glyph and item box,
never the form-control ladder — so point the height at `--lr-form-control-height-s` (or any tier of
that ladder) when the filter field has to match a themed search field beside it.

Emoji interaction states are separate: `--lr-emoji-picker-hover-bg`,
`--lr-emoji-picker-keyboard-active-bg`, `--lr-emoji-picker-selected-bg`/
`--lr-emoji-picker-selected-color`, and `--lr-emoji-picker-pressed-bg`, with matching
`--lr-emoji-picker-keyboard-active-outline-color`,
`--lr-emoji-picker-selected-outline-color`, and
`--lr-emoji-picker-pressed-outline-color` hooks for active, selected, and pressed. The legacy
`--lr-emoji-picker-active-bg` remains the fallback for hover and keyboard-active. These are inline
`var()` fallbacks rather than host declarations, so values set on any ancestor remain effective.
The committed form `value` alone drives `aria-selected`; roving focus and pointer navigation use
`data-active`, so moving through the grid never falsely changes selection. Forced-colors mode also
distinguishes hover (dashed), active (dotted), selected (solid), and pressed (double) outlines.

Two constraints remain. `--lr-emoji-picker-item-size` is held at the shared
`--lr-icon-button-size` minimum: smaller `size` tier values can still shrink the glyph, but never
the interactive option, and the windowed geometry follows the clamped, painted size. And windowed
rows are absolutely positioned at the row-height
pitch, so `--lr-emoji-picker-row-height` must stay at or above the item size plus the group-label
band (`--lr-space-l`) — the default's own formula — or consecutive rows overlap. Columns per
windowed row are additionally capped at 20 regardless of available width.
The `grid` scroll container also honors the opt-in theme-level
`--lr-theme-scrollbar-width`/`--lr-theme-scrollbar-gutter` hooks (defaults `auto`/`stable`, matching
its previous unconditional `scrollbar-gutter: stable`) — set either on `:root` or any ancestor for
one declaration to retheme every internal scroll container in the library.

**Optional peer dependency:** install `emoji-picker-element-data` with
`pnpm add emoji-picker-element-data` for the built-in auto-loaded default emoji set — omit it and
supply `groups` directly instead. The loader never throws; a missing or failed peer logs one
`console.warn` and leaves `groups` empty, and the picker then **fails closed and visibly**: the
grid renders a distinct localized `[part="load-error"]` surface instead of the ordinary
`[part="empty"]` message, so a skipped install is distinguishable at a glance from a genuine
zero-match search or a deliberate `groups = []` opt-out, and announces the same message once
through the document's shared assertive live region (not a shadow-root `role="alert"`, which
announces unreliably). Assigning `groups` afterwards clears it. The adapter buckets the peer's flat
entry list by numeric group id and returns only the public `{ key, label, emojis }` shape. The picker
privately maps auto-loaded group ids 0–9 to the existing `emojiPickerGroup*` locale strings; override
those through `registerLyraLocale()` or `.strings`. An unknown future group id uses `Group {id}`.

**Additional API surface:**

- `--lr-emoji-picker-control-gap` — Gap between field sections. Default: `var(--lr-space-xs)`.
- `--lr-emoji-picker-search-clear-gap` — Gap between the search field and the clear button inside
  `search-wrapper`. Default: `var(--lr-space-xs)`.
- `--lr-emoji-picker-radius` — Outer picker corner radius. Default: `var(--lr-radius)`.
- `--lr-emoji-picker-item-radius` — Search and emoji corner radius. Default: `var(--lr-radius-xs)`.
- `--lr-emoji-picker-search-border-color` — Resting search border color, independent of the hover
  color below. Default: `var(--lr-color-border)`.
- `--lr-emoji-picker-search-fill` — Resting search background. Default: `var(--lr-color-surface)`.
- `--lr-emoji-picker-search-hover-border-color` — Search hover border. Default: `var(--lr-color-brand)`.
