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

# `lr-carousel`

- **Import** `import '@aceshooting/lyra-ui/components/lr-carousel.js';` (stable tag alias; registers the tag)
- **Class** `LyraCarousel`, also available unregistered from `@aceshooting/lyra-ui/components/layout/carousel/carousel.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** 18 parts, 18 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-carousel`

Accessible scroll-snap carousel for arbitrary slotted slide elements. Mirrors `wa-carousel` /
`sl-carousel`, including their opt-in navigation and pagination, multi-slide pages, logical
orientation, autoplay, loop, mouse dragging, slots, methods, parts, and custom properties. Slide
semantics (`role="group"`, a localized "slide" role description, and a localized "Slide N of M"
label) are added only to `<lr-carousel-item>` children. An arbitrary slotted element keeps its own
native or authored semantics, and an explicit `role`, `aria-roledescription`, or `aria-label` on an
`<lr-carousel-item>` wins over generated metadata. Later author changes to those attributes and to
`hidden`, `inert`, or `aria-hidden` remain in effect across carousel updates. The carousel
temporarily makes off-page slides inert and aria-hidden, then restores their retained author state
when they become visible, are removed, or the carousel disconnects.

**Keyboard:** focus the `scroll-container` viewport to navigate with Arrow keys (horizontal LTR/RTL
or vertical), Home, and End. Keys originating in a native input, textarea, contenteditable surface,
or supported custom control remain owned by that editor, including when the editor is the assigned
slide itself; they do not change the active slide or move focus away from it.

**Properties:**

- `currentSlide: number = 0` (attribute `current-slide`, reflected) — zero-based index of the first
  slide in the active page. The pinned Web Awesome markup spelling `currentSlide` is also accepted
  through HTML's normalized
  `currentslide` attribute as a permanent compatibility alias. When both spellings are present on
  initial markup, canonical `current-slide` wins.
- `loop: boolean = false` (attribute `loop`, reflected) — wraps navigation at either end
- `autoplay: boolean = false` (attribute `autoplay`, reflected) and
  `autoplayInterval: number = 3000` (attribute `autoplay-interval`) — optional timed advance.
  Autoplay pauses while the page is hidden or the user is hovering, focusing, or dragging the
  carousel, and remains off under `prefers-reduced-motion: reduce`.
- `navigation: boolean = false` (attribute `navigation`, reflected) — renders previous and next
  buttons
- `pagination: boolean = false` (attribute `pagination`, reflected) — renders page indicators.
- `slidesPerPage: number = 1` (attribute `slides-per-page`) — number of simultaneously operable
  slides. Values used for layout are finite integers clamped to at least one and at most the live
  slide count.
- `slidesPerMove: number = 1` (attribute `slides-per-move`) — number advanced by `next()` and
  `previous()`, clamped to `slidesPerPage`. A final partial movement lands on the last full page.
- `orientation: 'horizontal'|'vertical' = 'horizontal'` — inline-axis or block-axis layout and
  scrolling. Give a vertical carousel a definite block size.
- `mouseDragging: boolean = false` (attribute `mouse-dragging`, reflected) — adds desktop
  click-and-drag scrolling without replacing native touch and trackpad scrolling. Pointer
  cancellation releases capture, removes drag state, and returns to the active snap position.
  Gestures begin only for a primary left-mouse pointer on noninteractive slide content; native,
  custom, shadow-wrapped, labelled, disabled, and editable controls retain their own pointer input.
- `slides: number` (read-only) — live assigned-slide count, updated after dynamic child changes.
- `accessibleLabel?: string` (attribute `accessible-label`) — fallback landmark name. Omitting it
  reads back `undefined` and uses the localized `carouselLabel` default; an explicitly empty value
  is used as-is. A host `aria-label` takes precedence by presence, including an explicitly empty
  value

**9.0 cleanup:** the redundant Lyra-only `index`, `showIndicators`, and `goTo()` aliases were
removed. Use mapped `currentSlide`, `pagination`, and `goToSlide()`. The writable/reflected
`slides` readout also became a readonly composition-derived property. Navigation and pagination
retain their mapped opt-in defaults, and the autoplay interval remains 3000ms.

**Methods:**

- `next(behavior: ScrollBehavior = 'smooth')` and
  `previous(behavior: ScrollBehavior = 'smooth')` move by `slidesPerMove`
- `goToSlide(index, behavior: ScrollBehavior = 'smooth')` moves to a specific slide
- `addSlide(slide: LyraCarouselItem)` appends a slide and `removeSlide(index)` removes one; page
  count, active range, inertness, eligible loop snapshots, and pagination reconcile
  automatically

**Events:** `lr-slide-change` (`detail: { index, slide }`) — emitted after the active slide changes
from a method, button, key, pagination item, autoplay tick, or settled user scroll. `slide` is the
original assigned element at `index`, never a loop endcap.

**Paging and scrolling.** In non-loop mode the page count is the set of reachable starts from zero
to `slideCount - slidesPerPage`, stepping by `slidesPerMove` and always including the final start.
Loop pagination exposes every slide as an exact valid start, so the current loop start always has
one and only one current indicator. Multi-slide basis conserves the allocation as
`(100% - (slidesPerPage - 1) * gap) / slidesPerPage`, including final partial pages. All
slides in the active page are restored to their authored `inert`/`aria-hidden` state; every other
slide keeps its layout box but becomes `inert` and `aria-hidden="true"`, so visible multi-slide
pages remain fully operable while off-page links are unreachable. Native mandatory scroll snap
owns touch, trackpad, momentum, and rubber-band behavior. Settling adopts the nearest page once and
emits one event for the whole gesture. Programmatic movement scrolls the same track; first mount
and reduced-motion alignment are instant. Loop mode adds inert, accessibility-hidden snapshots only
for side-effect-free plain HTML, so forward/backward wrapping can continue in the requested
direction before silently resetting to the matching original slide. Those snapshots refresh after
light-DOM content or attribute changes, and their idrefs/form-identifying attributes are removed. A
slide containing a custom element, media/resource owner, form state, script/style, or non-HTML
descendant is never cloned; wrapping falls back to the original slide instead, avoiding duplicate
lifecycle, network/playback, and state owners even when the physical wrap cannot use an endcap in
the requested direction.

Manual active-page changes after mount are appended to Lyra's shared light-DOM polite
announcement sink. The focusable `scroll-container` is not itself a shadow-root live
region. Initial connection and reconnection stay silent. Timer-driven autoplay advances also stay
silent, while click, keyboard, method, scroll-gesture, and property changes are announced even
when `autoplay` remains enabled. A change made while the carousel or a composed ancestor is
`hidden`, `inert`, `aria-hidden`, or CSS-hidden stays silent. Slide announcement text likewise
omits accessibility-hidden descendants. A subtree-pruned active slide root suppresses the entire
page announcement rather than synthesizing a position for content outside the tree; a
`visibility:hidden|collapse` root can still contribute a descendant that explicitly restores
`visibility:visible`, in which case the position and that exposed descendant are announced.
Nested forwarding slots contribute their flattened assigned content. Slot fallback text contributes
only when there is no direct assignment; an accessibility-hidden assignment remains authoritative
and does not expose the fallback. The `carouselSlideAnnouncement` message (English default:
`{position}: {content}`) controls the order and punctuation of each position/content pair, and
`carouselSlideAnnouncementSeparator` (English default: `. `) separates multiple visible-slide
summaries. A registered locale or the instance's `strings` override can customize both.

Horizontal Left/Right keys follow logical direction and swap under RTL; vertical carousels use
Up/Down without an RTL inversion. Home and End move to the first and final reachable start. The
populated multi-slide state remains accessible at a 320px allocation.
If a controlled page/page-size change or slide removal would make the currently focused slide
inert or disconnected, focus moves to the stable `scroll-container` before exclusion. A
newer external focus destination is never reclaimed.

**Slots:** default slides, `previous-icon`, and `next-icon`. Named icon slots replace only the
decorative glyph content; their flattened subtrees remain visible but are inert and aria-hidden.
Lyra retains the localized native-button names, actions, and minimum hit areas.

**CSS parts:** `base carousel` (same region node), `scroll-container` (focusable scroll port),
`navigation`, `navigation-button`, `navigation-button-previous` /
`navigation-button-next`, Shoelace aliases `navigation-button--previous` /
`navigation-button--next`, plus `previous-glyph` / `next-glyph`; `pagination`, `pagination-item`,
active aliases `pagination-item-active` / `pagination-item--active`, and `indicator-dot`. `track` and
`controls` are Lyra extensions.

**Themeable custom properties:** mapped `--aspect-ratio` (default `16/9`), `--scroll-hint`
(logical scroll-area padding), and `--slide-gap` (default `var(--lr-space-m)`). Lyra extensions
`--lr-carousel-indicator-current-bg` (default `var(--lr-color-brand-quiet)`) and
`--lr-carousel-indicator-current-border-color` (default `var(--lr-color-brand)`) color only the
active `indicator-dot`. `--lr-carousel-slide-basis` remains a compatibility escape hatch that
overrides the basis computed from `slidesPerPage`; prefer the property for normal multi-slide
layouts because it also updates paging and accessibility state.
Navigation buttons use independent `--lr-carousel-navigation-hover-bg`,
`--lr-carousel-navigation-hover-border-color`, `--lr-carousel-navigation-active-bg`, and
`--lr-carousel-navigation-active-border-color` hooks. Pagination dots use the corresponding
`--lr-carousel-pagination-hover-bg`, `--lr-carousel-pagination-hover-border-color`,
`--lr-carousel-pagination-active-bg`, and `--lr-carousel-pagination-active-border-color` hooks.
All are inline fallbacks at their state rules, inherit from ancestors, and retain the previous
brand/active-mix rendering when unset. The scroll-snap viewport's mouse-hover preview has its own
four-longhand outline shape, matching `lr-scroller`'s viewport:
`--lr-carousel-scroll-container-hover-outline-width` (default `var(--lr-border-width-thin)`),
`--lr-carousel-scroll-container-hover-outline-style` (default `solid`),
`--lr-carousel-scroll-container-hover-outline-color` (default `var(--lr-color-border-strong)`, set
to `transparent` to opt out entirely), and `--lr-carousel-scroll-container-hover-outline-offset`
(default `var(--lr-focus-ring-offset)`). Unset, all four resolve to the rule's previous literal
paint. The `scroll-container` part also honors the opt-in theme-level
`--lr-theme-scrollbar-width`/`--lr-theme-scrollbar-gutter` hooks (defaults `none`/`auto`, matching
its previous unconditional `scrollbar-width: none`) — set either on `:root` or any ancestor for one
declaration to retheme every internal scroll container in the library. Chromium and Safari ignore
the standard `scrollbar-width` property for any element a page also styles through the legacy
`::-webkit-scrollbar` pseudo-element, which this part's own stylesheet still does to hide its
scrollbar there, so on those two engines the hook only visibly retunes this part in Firefox.

```html
<lr-carousel navigation pagination aria-label="Screenshots">
  <lr-carousel-item
    ><img alt="Dashboard overview" src="overview.png"
  /></lr-carousel-item>
  <lr-carousel-item
    ><img alt="Dashboard details" src="details.png"
  /></lr-carousel-item>
</lr-carousel>
```

```html
<lr-carousel
  navigation
  pagination
  mouse-dragging
  slides-per-page="3"
  slides-per-move="2"
  aria-label="Projects"
>
  <lr-card>Solar</lr-card>
  <lr-card>Wind</lr-card>
  <lr-card>Battery</lr-card>
  <lr-card>Hydro</lr-card>
</lr-carousel>
```

---
