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

# `lr-tooltip`

- **Import** `import '@aceshooting/lyra-ui/components/lr-tooltip.js';` (stable tag alias; registers the tag)
- **Class** `LyraTooltip`, also available unregistered from `@aceshooting/lyra-ui/components/overlays/overlay/tooltip.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** 8 parts, 12 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-tooltip`

A tooltip for a consumer-owned trigger, positioned with the shared Floating UI positioner. Which
interactions open it is configurable as of 8.0.0; by default it is still hover and focus.

An open lr-tooltip repositions when its effective host or inherited text direction changes,
preserving open state without emitting lifecycle events.

Removing `content` safely omits tooltip fallback text while preserving `null` property readback;
later text renders normally.

**Properties:**

- `open: boolean = false` (reflected) — assigning it runs the same lifecycle as `show()`/`hide()`.
  Assigning `false` also cancels a delayed open that has not fired yet, even when the tooltip is
  already closed, so a pending timer can't reopen it behind the caller's back.
- `trigger: string = 'hover focus'` — **new in 8.0.0.** A _space-separated_ list of `hover`,
  `focus`, `click` and `manual`. `manual` (or an empty list) leaves the tooltip entirely under
  programmatic control. Note the name collision: this string property and the `trigger` _slot_ are
  different things — the slot holds the element, this property says which of its interactions count.
- `manual: boolean = false` — equivalent to including `manual` in `trigger`; kept because it reads
  better on a tooltip that is only ever driven from script
- `showDelay: number = 150` (attribute `show-delay`) and `hideDelay: number = 0` (attribute
  `hide-delay`) — **breaking in 8.0.0:** the single `delay` property is gone, split into these two
  independent milliseconds values, so a tooltip can linger after the pointer leaves without also
  being slow to appear. `showDelay` keeps the old `delay` default of 150ms; `hideDelay` defaults to
  `0`, so leaving the trigger now closes the tooltip at once, where 7.x's single `delay` also held
  it open for 150ms first. A non-finite value falls back to the default; a negative one clamps to
  `0` (immediate) and an oversized one to the largest delay `setTimeout` can represent, so neither
  can hang the tooltip open.
- `placement: Placement = 'top'` (reflected) — the full Floating UI vocabulary, mirrored under RTL
- `distance: number = 8` — anchor-offset distance in px; identical semantics to
  `<lr-popover>.distance` (both wrap the same `place()`/`offset()` middleware)
- `skidding: number = 0` — offset along the anchor's edge, in px. New in 8.0.0.
- `for: string = ''` (reflected) — id of an element in this tooltip's own root. It positions behind
  a direct `.anchor`; when it resolves to a live HTML element and no trigger is slotted, it also owns
  the configured interaction listeners and `aria-describedby`. A slotted trigger wins interaction
  and ARIA ownership. Assigning `null` clears the attribute to the canonical `''` read value; the
  getter itself remains non-nullable
- `anchor: Element | null = null` (property only) — positioning-only direct anchor, taking priority
  over `for` and the active interaction owner without receiving listeners or generated ARIA
- `disabled: boolean = false` (reflected) — prevents both interaction and programmatic opening;
  setting it while open closes the tooltip
- `hoist: boolean = false` (reflected) — switches the mapped absolute positioning default to fixed
- `positioningStrategy: PlaceStrategy = 'absolute'` (attribute `positioning-strategy`, reflected) —
  see `<lr-popover>`. `hoist: boolean = false` is its retained exact alias
  (`hoist` ⇔ `positioning-strategy="fixed"`); writing either spelling updates the other, so the two
  attributes can never disagree. Prefer `positioning-strategy` in new code. It also honors the
  cascading `--lr-positioning-strategy` custom property `<lr-popover>` documents, ahead of this
  mirrored `absolute` default, when neither spelling is authored on the instance.
- `arrow: boolean = true` (reflected), `withoutArrow: boolean = false` (attribute `without-arrow`,
  reflected), `arrowPlacement: 'anchor'|'start'|'end'|'center' = 'anchor'`
  (attribute `arrow-placement`) and `arrowPadding: number = 0` (attribute `arrow-padding`) — the
  same arrow trio `<lr-popover>` documents (`llms/components/lr-popover.md`), new in 8.0.0
- `content: string = ''` — plain-text tooltip content, used when nothing is slotted
- `accessibleLabel: string = ''` (attribute **`aria-label`**) — a host `aria-label` wins by
  attribute presence, including an explicitly empty value. When the attribute is absent, an
  actionable popup with no property label uses the localized `popover` string.

To preserve the previous Lyra-shaped visual defaults explicitly, use `distance="6" without-arrow`;
origin-aware migration emits those tokens.

**Methods:**

- `show(): Promise<void>` — open immediately, bypassing `show-delay` and interaction policy, then
  resolve after `lr-after-show`
- `hide(): Promise<void>` — close immediately, bypassing `hide-delay`, then resolve after
  `lr-after-hide`
- `showAt(rect: { x, y, width?, height?, contextElement? }, options?: { returnFocusTo?: HTMLElement })`
  — same virtual-anchor contract as `lr-popover.showAt()` (`llms/components/lr-popover.md`)
  (anchors to an arbitrary rectangle
  instead of any DOM anchor, `width`/`height` default to `0`, `contextElement` gives
  `autoUpdate()` something to observe, Escape returns focus to `options.returnFocusTo` or skips
  focus-return, re-call with fresh coordinates to re-anchor a moving point). Opens immediately,
  bypassing `show-delay`/`trigger`/`manual` (all are interaction-debounce concerns for a slotted
  trigger, not a deliberate programmatic call); while active it removes every DOM interaction/ARIA
  owner. Close it with `hide()` or `open = false`. Non-finite coordinates or dimensions are a no-op.

**Events:** `lr-show` (cancelable), `lr-after-show`, `lr-hide` (cancelable), `lr-after-hide` — the
same four-event contract, timing and veto semantics `<lr-popover>` documents
(`llms/components/lr-popover.md`), and all four are
new to this component in 8.0.0. A vetoed `lr-show` leaves the tooltip closed whether the delay
elapsed, `show()` was called, or `open` was assigned.

Tooltip motion resolves `tooltip.show` / `tooltip.hide` through the public animation registry.
The same per-element/global precedence, RTL keyframe selection, token-timing fallback,
reduced-motion flattening, and null-disable lifecycle rules documented for `lr-popover` apply.

**Slots:** both mapped shapes are supported. Web Awesome uses named `trigger` plus default tooltip
content. Shoelace uses the default slot for the trigger and `slot="content"` (or the `content`
property) for tooltip content. A named trigger always selects the first shape; without one, an
explicit content source makes the default slot unambiguously the trigger.

**CSS parts:** `popup base tooltip base__popup` are aliases on the same wrapper; `trigger`; `body`;
and `arrow base__arrow` (rendered unless suppressed). The arrow also carries the resolved side —
`arrow-top`, `arrow-bottom`, `arrow-left`, or `arrow-right`.

**Themeable custom properties:** mapped `--max-width`, `--show-delay`, `--hide-delay`, and
`--arrow-size`; retained `--lr-tooltip-max-inline-size`, `--lr-tooltip-background`,
`--lr-tooltip-color`, and `--lr-tooltip-arrow-size` remain fallbacks. A tooltip popup has no inner
scroll wrapper to move overflow onto, so its default arrow trades internal scrolling for a visible
arrow — use `<lr-popover>` when a floating surface needs both.

`--lr-overlay-surface`, `--lr-overlay-border` and `--lr-overlay-radius` are listed on this tag
because its rules live in the stylesheet module `lr-popover` also composes, but a tooltip bubble is
a **deliberate exclusion** from the overlay-surface family: it is a high-contrast label, not a
panel, so it keeps painting from `--lr-tooltip-background`/`--lr-tooltip-color`, draws no border,
and keeps the tighter `var(--lr-radius-xs)` corner. Setting any of the three changes nothing here.

`--lr-positioning-strategy` (16.0.0) is not excluded: the tooltip honors the same cascading
`absolute`/`fixed` override `<lr-popover>` documents (`llms/components/lr-popover.md`), ahead of
its own mirrored `absolute`
default, when neither `positioning-strategy` nor `hoist` is authored on the instance.

```html
<lr-tooltip
  trigger="hover focus click"
  show-delay="0"
  hide-delay="400"
  arrow
  placement="right"
>
  Copied to clipboard
  <button slot="trigger" type="button">Copy</button>
</lr-tooltip>

<!-- Shoelace-compatible shape: default trigger, named content. -->
<lr-tooltip content="Save your changes">
  <button type="button">Save</button>
</lr-tooltip>
```

While open, trigger `aria-describedby` points to a hidden text proxy in the tooltip's light DOM,
not the shadow-private popup. Native triggers resolve that ID directly. A description is only
announced on the node that actually holds focus, so when the trigger is a custom element the same
proxy is applied to the first focusable descendant as well — across slots and nested open shadow
roots — which covers `lr-select`, `lr-switch`, `lr-chip` and any consumer-authored wrapper, not
just the components that forward their own host `aria-describedby`. A descendant in the same tree
receives the serialized ID; one inside a shadow root is linked through `ariaDescribedByElements`,
whose explicit element-reference assignment intentionally leaves that control's serialized
`aria-describedby` value empty in supporting browsers. Existing author-provided descriptions —
including a control's own internal hint/error text — are merged while open and restored when the
tooltip closes, the trigger is replaced, or the tooltip disconnects. Late author writes remain the
release baseline while Lyra's active description stays composed into the owned value.

With no slotted trigger, a live HTML `for` target receives those same interactions and description;
target insertion, removal, replacement and `id` changes are tracked without requiring reinsertion.
Removing the sole connected direct or interaction anchor force-closes despite an `lr-hide` veto,
while a live slotted/`for` positioning fallback is rebound and keeps the tooltip open.

Plain content keeps `role="tooltip"`. If actionable content appears anywhere in the assigned
default-slot subtree — including native links/form/media controls, authored sequential focus stops,
explicit ARIA widget roles, or content inside a nested custom element's open shadow root — the popup
promotes to a named `role="dialog"` and remains open while pointer or focus is within it. Escape
from either the trigger or popup closes it; Escape from popup content returns focus to the trigger's
real composed focus target. Bubbling `focusin`/`focusout` keeps the tooltip open when focus moves
within a wrapper trigger or between interactive popup controls.
The content scan follows the live composed assignment through forwarding slots. Reassignment,
external descendant text/actionability changes, and relevant composed-ancestor visibility changes
update both the hidden description proxy and popup role; when a forwarding slot becomes genuinely
unassigned, its own fallback content is restored. This classification also runs while the popup is
closed, without treating the popup's internal closed-state visibility as consumer-hidden content.
Image alternatives and `aria-labelledby` references contribute their accessible text; referenced
targets outside the tooltip subtree are observed too, so a sibling label's live text mutation
updates the proxy. Text, actionability, and observer enrollment share bounded, cycle-safe composed
traversal; content beyond the traversal ceiling fails closed instead of recursing without bound.
While open, rootless custom-element content receives a bounded initialization grace period for an
upgrade or newly attached open shadow root; later observable content mutations start a fresh
grace period. This catches normal lazy initialization without scheduling perpetual animation-frame
work for a legitimate custom element that intentionally has no shadow root. Content and trigger
observers plus delayed show/hide timers are bound to the tooltip's current owner window; disconnect
or cross-document adoption cancels the old realm, and reconnect creates fresh observers. Use
`lr-popover` when click-to-open ownership is desired.

**`showAt()` composed with `lr-graph`** — anchoring a popover to a clicked graph node. Note:
`lr-graph.getNodePosition()` and the `lr-node-click` event's `{ x, y }` are in the graph's own
_local drawing space_ (pre pan/zoom), not viewport pixels, so they can't be passed to `showAt()`
directly. For `renderer="svg"` (the default), read the clicked node's own rendered element instead,
whose `getBoundingClientRect()` is already viewport-relative; for `renderer="canvas"` (no per-node
DOM element), use the click event's own `clientX`/`clientY`.

```js
const graph = document.querySelector("lr-graph");
const detail = document.querySelector("lr-popover"); // no slotted trigger needed for showAt()

graph.addEventListener("click", (event) => {
  const nodeEl = event
    .composedPath()
    .find((el) => el instanceof Element && el.matches('[part="node"]'));
  if (!nodeEl) return; // clicked empty canvas/background, not a node
  const rect = nodeEl.getBoundingClientRect();
  detail.showAt({
    x: rect.left + rect.width / 2,
    y: rect.top,
    width: rect.width,
    height: rect.height,
  });
});
```
