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

# `lr-card`

- **Import** `import '@aceshooting/lyra-ui/components/lr-card.js';` (stable tag alias; registers the tag)
- **Class** `LyraCard`, also available unregistered from `@aceshooting/lyra-ui/components/layout/card/card.class.js`
- **Family** `components/layout/` — 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, 14 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-card`

A generic, styled bordered content container — the "small bordered surface with padding" idiom common
to hero highlights, clickable grid tiles, and management-list items. A direct `<lr-*>` counterpart
to `<wa-card>`'s contract, staying slot-compatible with `lr-result-card` where they overlap.

**Properties:**

- `appearance: 'accent' | 'filled' | 'outlined' | 'filled-outlined' | 'plain' = 'outlined'`
  (reflected) — `'outlined'` (the default) is a plain bordered surface; `'filled'` swaps the border
  for a quiet brand-tinted background; `'filled-outlined'` keeps the border and adds that same tinted
  background; `'accent'` drops the border for a single colored accent stripe on the leading edge;
  `'plain'` has no border or background at all.
- `orientation: LyraOrientation = 'vertical'` (reflected; the shared
  `'horizontal' | 'vertical'` layout axis) — vertical renders media,
  header/actions, body, and footer/footer-actions as sections. Horizontal arranges media/image,
  body, and `actions` in logical order and stacks them when the card's own container drops below
  30rem.
- `withHeader`, `withHeaderActions`, `withMedia`, `withFooter`, and `withFooterActions` (boolean,
  reflected as `with-header`, `with-header-actions`, `with-media`, `with-footer`, and
  `with-footer-actions`) — SSR presence hints. They expose an otherwise-empty section wrapper before
  slot assignment can be measured; populated slots are still detected automatically after hydration.
- `actionable: boolean = false` (reflected) — opt-in no-link whole-card action behavior: the hover/focus-visible
  treatment (border-color shift, `cursor: pointer`) plus, when `href` is **not** also set, real
  activation semantics. Those come from a real native `<button part="activation-button">` stretched
  across the card, not from making `[part='base']` itself focusable: it is the keyboard tab stop,
  it answers Enter and Space natively, and activating it emits `lr-card-activate`. With `href` set,
  a stretched sibling native `<a>` owns navigation, no activation button renders, and
  `lr-card-activate` never fires. Consumer slots stay outside that link, so their controls remain
  independent. A valid `href` is inherently actionable and receives the same interaction paint
  without this flag. `false` (the default) leaves a no-link card static: no button, listeners, or
  events.
- `disabled: boolean = false` (reflected) — turns the card's OWN activation off. The native
  `activation-button` renders `disabled`; a linked card's stretched `<a>` loses its `href`, so it
  genuinely cannot navigate rather than merely claiming to be disabled while still clickable, and
  gains an explicit `role="link"` so its accessible name and `aria-current` stay valid on an
  element that no longer has an implicit role. `lr-card-activate` stops firing from every path,
  `click()` included, the control leaves the tab order, and the card paints at
  `--lr-opacity-disabled` with a `not-allowed` cursor. Scoped to the card's own action: a passive
  card (no `actionable`, no `href`) has nothing to turn off, so `disabled` leaves it untouched,
  and slotted controls stay yours to disable. `<lr-card>` is deliberately not form-associated (it
  is a layout container, not a form control), so an ancestor `<fieldset disabled>` does not
  cascade into it — disable each card explicitly.
- `accessibleLabel: string | null = null` (attribute `aria-label`) — the accessible name of the
  native whole-card owner: the activation button without `href`, or the stretched link with it.
  An explicitly empty value is retained; only an absent value falls back to card or linked content,
  so set it explicitly for a card whose content is an image or a chart.
- `href?: string` — when set, a real stretched `<a href=...>` renders behind the consumer slots for
  a whole-card link (e.g. a wide CTA tile). A click on noninteractive card content follows that
  link, while slotted controls keep their own native or Lyra behavior. Unset (the default) renders
  a plain `<div>`.
- `target?: string` — native anchor target, applied only while `href` resolves to a link. Setting it
  to `'_blank'` (or any other target) forces `noopener noreferrer` on the rendered anchor.
- `rel?: string` — author relationship tokens such as `nofollow`, `sponsored`, `me`, or `license`.
  `opener` is always stripped, other tokens are preserved, and any set `target` force-adds the
  non-negotiable `noopener noreferrer` floor. With no target, safe author tokens render unchanged.
- `aria-pressed` and `aria-current` (attributes only) — forwarded reactively onto the native
  control the card actually renders, the same mechanism `<lr-button>` and `<lr-icon-button>` use.
  `aria-pressed` accepts `'true' | 'false' | 'mixed'` and reaches the `activation-button` only —
  `link` has no pressed state, so a linked card never receives it — the same **16.0.0** carve-out
  `<lr-button>` and `<lr-icon-button>` took. The global `aria-current`
  accepts `'page' | 'step' | 'location' | 'date' | 'time' | 'true' | 'false'` and reaches both the
  activation button and the stretched link. Anything outside those sets is dropped rather than
  passed through, so a typo never reaches the accessibility tree. This is what lets a single-select
  list of card-shaped tiles announce which one is the active selection.

**Events:** `lr-card-activate` (no detail) — the whole card was activated, by a click anywhere on it
or by Enter/Space on `[part='activation-button']`. Only fired while `actionable` is set **without**
`href`. Never fired for an interaction that originated in a slotted control, so a card can keep its
own action buttons (see the gotchas below).

**Methods:** `click(): void` activates the native whole-card owner: the linked anchor when `href` is
safe, or the activation button while `actionable` is set without a link. Passive cards remain
inert.

Changing `href` or `actionable` while the whole-card owner has focus transfers focus across the
link, activation button, and a programmatically focusable passive base. It never overrides a newer
external focus destination.

**Slots:** default (the card body), `header` (vertical header content), `media` and `image` (aliases
for media above the header vertically or at logical start horizontally), `footer` (vertical footer
content), `header-actions` and `footer-actions` (controls aligned with those vertical sections), and
`actions` (horizontal-card actions; retained as the legacy header-actions spelling vertically).

**CSS parts:** `base` (the outer container — a `<div>`, or a stretched `<a>` behind consumer slots
when `href` is set),
`activation-button` (the native whole-card action, rendered only while `actionable` without `href`;
it is absolutely positioned across the card, `pointer-events: none` so it never intercepts a click
meant for slotted content, and it owns the card's `:focus-visible` ring), `media` and `image`
(aliases on the wrapper around both media slots, hidden entirely when empty), `header` (wrapper around the `header` slot and
`actions`, hidden entirely when both are empty), `actions` (wrapper around the `actions` slot,
hidden entirely when empty), `body` (wrapper around the default slot), `footer` (wrapper around the
`footer` and `footer-actions` slots, hidden entirely when both are empty).

**Themeable custom properties:** `--spacing` (default `var(--lr-space-m)`) controls the padding and
gap around card sections. Shoelace-compatible `--padding` is its fallback; `--border-color`,
`--border-radius`, and `--border-width` control the outer and section borders. Otherwise shared
tokens — `--lr-color-border`/`-surface`/`-brand`/
`-brand-quiet`, `--lr-radius`, `--lr-space-s`/`-m`, `--lr-transition-fast`,
`--lr-focus-ring-*`.
Appearance and interaction paint can be rethemed independently through `--lr-card-outlined-bg`
(the DEFAULT `outlined` appearance's background, and `accent`'s, which adds a stripe without
restating a surface — defaults to `var(--lr-color-surface)`, mirroring `<lr-details>`'s
`--lr-details-outlined-bg`), `--lr-card-filled-bg`,
`--lr-card-filled-outlined-bg`, `--lr-card-accent-border-color`,
`--lr-card-interactive-hover-border-color`, `--lr-card-interactive-active-border-color`, and
`--lr-card-interactive-active-overlay`. They inherit from ancestors and fall back to the exact
former brand and active-mix values when unset.
`--lr-card-shadow` is **undeclared by default**, so `box-shadow` falls back to `none` —
byte-identical to before this property existed — mirroring `--lr-button-shadow`'s pattern; set it
for a raised card without a `::part(base)` rule. `--lr-card-interactive-hover-shadow` styles an
`actionable`/linked card's shadow while hovered, falling back to `--lr-card-shadow` itself so a
card given only a resting shadow keeps that exact shadow on hover.

**Optional peer deps:** none.

```html
<lr-card appearance="outlined" href="/reports/42" with-media with-header>
  <img slot="image" src="/thumb.png" alt="" />
  <span slot="header">Q3 Report</span>
  <span slot="header-actions"><lr-chip tone="success">Ready</lr-chip></span>
  Revenue up 12% quarter-over-quarter.
  <span slot="footer">Updated 2 days ago</span>
  <button slot="footer-actions" type="button">Download</button>
</lr-card>
```

**Known gotchas:**

- every `appearance` renders on the _same_ `[part="base"]` element — there's no separate element per
  variant, so a `::part(base)` override applies uniformly regardless of `appearance`.
- **a card clips, it does not scroll — and it never picks a scroll owner for you.** `[part='base']`
  stretches to the host's allocated block-size and clips its overflow, which is what keeps a
  full-bleed `media`/`image` child inside the rounded border. In an auto-sized row the card simply
  grows and nothing is clipped; give it a _definite_ allocation (a fixed grid row, an explicit
  `block-size`) and body content taller than that allocation is clipped silently, with no
  scrollbar. Neither upstream card exposes an overflow, block-size, or scroll hook and neither does
  this one: the public `body` part already carries the whole decision, and a `::part()` rule from
  your tree wins over the shadow stylesheet regardless of specificity. A fixed-height tile that
  must hold more content says so itself:

  ```css
  .tile-grid {
    display: grid;
    grid-template-rows: 12rem;
  }
  .tile-grid lr-card::part(body) {
    overflow: auto;
    overscroll-behavior: contain;
  }
  ```

  `overflow` other than `visible` also zeroes the body's automatic minimum size, so that one
  declaration is enough — the body shrinks into the tile and scrolls, and `max-block-size` /
  `scrollbar-gutter` stay available on the same rule. The linked (`href`) card behaves identically.
- slot-presence (`header`/`media`/`image`/`footer`/`actions`/`header-actions`/`footer-actions`) is
  tracked in JS, not via CSS `:empty` (a
  `[part]` wrapper always contains a literal `<slot>` child, so `:empty` never matches) — the same
  pattern `lr-empty`/`lr-widget` use.
- The `with-*` hints force section presence; do not set one for a section that should stay absent.
  They are safe to leave in server-rendered markup once hydrated, because actual slot detection is
  combined with—not substituted for—the hints.
- Horizontal orientation intentionally omits the vertical header/footer presentation and uses the
  `actions` slot beside the body. Its 30rem breakpoint is a container query on the card allocation,
  not a viewport media query, so the same card can be horizontal in a wide region and stacked in a
  narrow sidebar on one page.
- **`[part='base']` itself deliberately carries no `role="button"` and is not focusable.** A card is
  a _container_ — it routinely holds slotted buttons and links — and `role="button"` around
  focusable descendants is the `nested-interactive` accessibility violation this library's own a11y
  gate enforces. (`lr-chip`'s `toggleable` mode _can_ carry `role="button"` because it forbids
  focusable children outright.) The whole-card action is therefore a _sibling_ of the slotted
  content — `[part='activation-button']` — so the actionable roles are never nested inside one
  another, and the card still announces as a real button rather than as an unnamed focusable region.
- because the base element carries no `role="button"` to disambiguate, "did the user aim at the card
  or at a control inside it?" is answered at event time: the composed path from the original target
  up to `[part='base']` is walked, and `lr-card-activate` is suppressed if anything along the way is
  itself a control (a link, `button`, `input`, `select`, `textarea`, `label`, `summary`,
  `contenteditable`, anything carrying a `tabindex` other than `-1`, or an ARIA widget role such as
  `button`/`link`/`checkbox`/`switch`/`radio`/`menuitem`/`option`/`tab`/`textbox`/`slider`/
  `spinbutton`). Using the _composed_
  path is what makes this work through a slotted component's own shadow root — a click on
  `<lr-button>` retargets to the host, but its composed path still contains the internal native
  `<button>`.
- a click whose composed path starts on `[part='activation-button']` skips that walk entirely and
  always activates — it _is_ the whole-card action, so there is nothing to disambiguate.
- with `href`, `[part='base']` is a stretched real anchor sibling behind the visible content rather
  than an ancestor of it. Clicks from noninteractive slotted content are delegated to that anchor;
  composed-path arbitration leaves native and Lyra buttons, links, and fields independent. The
  linked example's `Download` button therefore does not navigate the card.

---
