# Carousel

> A family of composable components for touch-enabled carousels with external navigation, pagination dots, synced instances, autoplay, and Swiper.js transition effects.

# Carousel

## Overview

Use the Carousel family when you need swipeable or keyboard-navigable slides with optional autoplay, looping, synced instances, and external navigation buttons. The root component owns the Swiper.js runtime configuration, while `Carousel Slide` defines each slide. External `Carousel Previous Button`, `Carousel Next Button`, and `Carousel Dots` components can be placed anywhere on the page and target a specific carousel by ID. Multiple carousels sharing a sync group ID stay aligned automatically — ideal for main-content-plus-thumbnail patterns.

## Authoring Structure

```
Carousel
├── Carousel Slide
│   └── slide content
├── Carousel Slide
│   └── slide content
└── Carousel Slide
    └── slide content

Carousel Previous Button   (anywhere on page)
Carousel Next Button       (anywhere on page)
Carousel Dots              (anywhere on page)
```

### Placement Rules

| Component | Placement | Role |
|---|---|---|
| **Carousel** | Top-level wrapper. | Owns the Swiper runtime, slide configuration, IDs, and sync settings. |
| **Carousel Slide** | Direct child of `Carousel`. | Each direct child becomes one navigable slide. |
| **Carousel Previous Button** | Anywhere on the page. | Targets a carousel by ID and moves it to the previous slide. |
| **Carousel Next Button** | Anywhere on the page. | Targets a carousel by ID and moves it to the next slide. |
| **Carousel Dots** | Anywhere on the page. | Renders clickable pagination bullets for a targeted carousel. |

## Quick Start

<CodeExample title="Basic — slides only" language="jsx">
{`<OmeCarousel settings='{{"slidesPerView":"1","spaceBetween":"16"}}'>
  {#slot default}
    <OmeCarouselSlide>
      {#slot default}Slide 1{/slot}
    </OmeCarouselSlide>
    <OmeCarouselSlide>
      {#slot default}Slide 2{/slot}
    </OmeCarouselSlide>
  {/slot}
</OmeCarousel>`}
</CodeExample>

<CodeExample title="With external navigation and dots" language="jsx">
{`<OmeCarousel
  identity='{{"id":"featured-gallery"}}'
  settings='{{"slidesPerView":"1","spaceBetween":"16"}}'
>
  {#slot default}
    <OmeCarouselSlide>
      {#slot default}{/slot}
    </OmeCarouselSlide>
    <OmeCarouselSlide>
      {#slot default}{/slot}
    </OmeCarouselSlide>
  {/slot}
</OmeCarousel>

<OmeCarouselPrevButton targeting='{{"for":"featured-gallery"}}' />
<OmeCarouselDots targeting='{{"for":"featured-gallery"}}' />
<OmeCarouselNextButton targeting='{{"for":"featured-gallery"}}' />`}
</CodeExample>

<CodeExample title="Synced carousels (main + thumbnails)" language="jsx">
{`<OmeCarousel
  identity='{{"id":"main-carousel","groupId":"product-sync"}}'
  settings='{{"slidesPerView":"1","loop":"true"}}'
>
  {#slot default}
    <OmeCarouselSlide>
      {#slot default}{/slot}
    </OmeCarouselSlide>
    <OmeCarouselSlide>
      {#slot default}{/slot}
    </OmeCarouselSlide>
  {/slot}
</OmeCarousel>

<OmeCarousel
  identity='{{"id":"thumb-carousel","groupId":"product-sync"}}'
  settings='{{"slidesPerView":"4","spaceBetween":"8","loop":"true"}}'
>
  {#slot default}
    <OmeCarouselSlide>
      {#slot default}{/slot}
    </OmeCarouselSlide>
    <OmeCarouselSlide>
      {#slot default}{/slot}
    </OmeCarouselSlide>
  {/slot}
</OmeCarousel>`}
</CodeExample>

---

## Family Components

### Carousel (Root)

The root container wraps all slides and manages the Swiper.js runtime: slide layout, navigation wiring, keyboard support, autoplay, effects, and sync groups. It renders a `div` (configurable via `tag`) with the `swiper` class and a child `swiper-wrapper`. The slot accepts `Carousel Slide` children directly.

<PropsTable group="Structure">
  <Prop name="tag" type="string" defaultValue="div">
    HTML tag for the carousel root element.
  </Prop>
</PropsTable>

<PropsTable group="Identity">
  <Prop name="id" type="string" defaultValue='""'>
    Stable carousel ID used by external buttons, dots, and synced carousels to target this instance. Required when using any external control.
  </Prop>
  <Prop name="groupId" type="string" defaultValue='""'>
    Shared sync group ID. Carousels with the same group ID stay synchronized — sliding one advances the others.
  </Prop>
</PropsTable>

<PropsTable group="Settings">
  <Prop name="initialSlide" type="string" defaultValue='"0"'>
    Zero-based slide index that is active on first load.
  </Prop>
  <Prop name="loop" type="boolean" defaultValue="false">
    Wraps from the last slide back to the first instead of disabling navigation at the ends.
  </Prop>
  <Prop name="centeredSlides" type="boolean" defaultValue="false">
    Centers the active slide in the viewport.
  </Prop>
  <Prop name="slidesPerView" type="string" defaultValue='"1"'>
    Number of slides visible at once. Use `"auto"` to size slides from CSS widths.
  </Prop>
  <Prop name="spaceBetween" type="string" defaultValue='"0"'>
    Gap between slides in pixels.
  </Prop>
  <Prop name="keyboardEnabled" type="boolean" defaultValue="true">
    Enables left and right arrow key navigation when the carousel root is focused.
  </Prop>
  <Prop name="enableResponsive" type="boolean" defaultValue="false">
    Enables breakpoint-based slides per view and spacing overrides.
  </Prop>
</PropsTable>

<PropsTable group="Responsive Controls">
  <Prop name="breakpoint" type="string">
    Minimum viewport width in pixels where this row starts applying.
  </Prop>
  <Prop name="slidesPerView" type="string">
    Slides per view at this breakpoint. Use `"auto"` to size slides from CSS widths.
  </Prop>
  <Prop name="spaceBetween" type="string">
    Gap between slides in pixels at this breakpoint.
  </Prop>
</PropsTable>

<PropsTable group="Autoplay">
  <Prop name="autoplay" type="boolean" defaultValue="false">
    Advances slides automatically.
  </Prop>
  <Prop name="autoplayDelay" type="string" defaultValue='"3000"'>
    Delay between autoplay transitions in milliseconds.
  </Prop>
  <Prop name="disableOnInteraction" type="boolean" defaultValue="false">
    Stops autoplay after user interactions (swipe, click).
  </Prop>
  <Prop name="pauseOnMouseEnter" type="boolean" defaultValue="false">
    Pauses autoplay while the pointer is over the carousel.
  </Prop>
  <Prop name="stopOnLastSlide" type="boolean" defaultValue="false">
    Stops autoplay when the last slide is reached in non-loop mode.
  </Prop>
  <Prop name="reverseDirection" type="boolean" defaultValue="false">
    Runs autoplay toward previous slides instead of next slides.
  </Prop>
</PropsTable>

<PropsTable group="Effects">
  <Prop name="effect" type={`"slide" | "fade" | "coverflow" | "flip" | "cube" | "cards" | "creative"`} defaultValue="slide">
    Active Swiper transition effect. `"slide"` is the lightweight default — other effects load their runtime only when selected.
  </Prop>
  <Prop name="crossFade" type="boolean" defaultValue="true">
    **Fade effect.** Fades slides over each other instead of briefly showing content underneath.
  </Prop>
  <Prop name="rotate" type="string" defaultValue='"50"'>
    **Coverflow effect.** Slide rotation in degrees.
  </Prop>
  <Prop name="depth" type="string" defaultValue='"100"'>
    **Coverflow effect.** Depth offset in pixels.
  </Prop>
  <Prop name="stretch" type="string" defaultValue='"0"'>
    **Coverflow effect.** Spacing stretch between slides in pixels or percent.
  </Prop>
  <Prop name="modifier" type="string" defaultValue='"1"'>
    **Coverflow effect.** Effect multiplier.
  </Prop>
  <Prop name="scale" type="string" defaultValue='"1"'>
    **Coverflow effect.** Slide scale effect.
  </Prop>
  <Prop name="slideShadows" type="boolean" defaultValue="true">
    **Coverflow / Flip / Cube / Cards effects.** Adds shadows to slides.
  </Prop>
  <Prop name="limitRotation" type="boolean" defaultValue="true">
    **Flip effect.** Limits edge slide rotation.
  </Prop>
  <Prop name="shadow" type="boolean" defaultValue="true">
    **Cube effect.** Adds a main cube shadow.
  </Prop>
  <Prop name="shadowOffset" type="string" defaultValue='"20"'>
    **Cube effect.** Main shadow offset in pixels.
  </Prop>
  <Prop name="shadowScale" type="string" defaultValue='"0.94"'>
    **Cube effect.** Main shadow scale ratio.
  </Prop>
  <Prop name="perSlideOffset" type="string" defaultValue='"8"'>
    **Cards effect.** Offset distance per slide in pixels.
  </Prop>
  <Prop name="perSlideRotate" type="string" defaultValue='"2"'>
    **Cards effect.** Rotation angle per slide in degrees.
  </Prop>
  <Prop name="rotate" type="boolean" defaultValue="true">
    **Cards effect.** Enables cards rotation.
  </Prop>
  <Prop name="creativeEffect" type="object" defaultValue="{}">
    **Creative effect.** Swiper `creativeEffect` object for advanced transforms.
  </Prop>
</PropsTable>

<PropsTable group="Custom Parameters">
  <Prop name="useCustomConfig" type="boolean" defaultValue="false">
    Uses the JSON config object as the source of truth for Swiper behavior while keeping identity-driven controls (navigation, dots, sync) wired automatically.
  </Prop>
  <Prop name="jsonConfig" type="object" defaultValue="{}">
    Custom Swiper configuration object. Effect selection and identity wiring stay controlled by Etch — the JSON refines behavior like breakpoints, autoplay, and keyboard options.
  </Prop>
</PropsTable>

<PropsTable group="Styling">
  <Prop name="class" type="class" defaultValue="ome-carousel">
    CSS class applied to the carousel root element. The `swiper` class is always included.
  </Prop>
</PropsTable>

#### Settings and Autoplay Visibility

The **Settings** and **Autoplay** prop groups are visible only when `useCustomConfig` is `false`. When custom config mode is on, those props are hidden and the `jsonConfig` object becomes the behavior source of truth instead.

#### Sync Groups

Carousels that share the same `identity.groupId` stay synchronized via Swiper's Controller module. This is useful for main-carousel-plus-thumbnail patterns — sliding one carousel automatically advances the other. At least two carousels must share a group ID for sync to activate.

#### Navigation Boundary State

In non-loop mode, external navigation buttons receive a `data-ome-carousel-nav-disabled` attribute at the boundaries:

- The **Previous** button is disabled at the first slide.
- The **Next** button is disabled at the last slide.

When `loop` is `true`, buttons never disable — the carousel wraps around instead.

#### Keyboard Behavior

When the carousel root has focus:

| Key | Action |
|---|---|
| `ArrowRight` | Advance to the next slide. |
| `ArrowLeft` | Go back to the previous slide. |

Keyboard navigation is enabled by default (`keyboardEnabled: true`) and scoped to the viewport (`onlyInViewport: true`). Disable it by setting `keyboardEnabled` to `false`.

#### Accessibility

The root component wires up ARIA attributes via Swiper's A11y module:

- The carousel container announces `"Content carousel"` as its role message.
- Navigation buttons have `aria-label` defaults (`"Previous slide"`, `"Next slide"`), overridable via each button's `label` prop.
- Pagination dots announce `"Go to slide N"` via `aria-label`.
- The Swiper A11y module provides live-region announcements for slide changes, including `"This is the first slide"` and `"This is the last slide"` messages.

---

### Carousel Slide

Each `CarouselSlide` represents a single slide inside the carousel. It renders as a `` with the `swiper-slide` class and accepts any content in its default slot.

<PropsTable group="Styling">
  <Prop name="class" type="class">
    CSS class names applied to the slide container.
  </Prop>
</PropsTable>

---

### Carousel Previous Button

An external `<button>` element that navigates a targeted carousel backward. It can be placed anywhere on the page — it does not need to be inside the carousel root. When the slot is empty, a default left-arrow SVG icon is rendered automatically.

<PropsTable group="Targeting">
  <Prop name="for" type="string" defaultValue='""'>
    Target carousel ID. This must match `identity.id` on the carousel root you want to control.
  </Prop>
</PropsTable>

<PropsTable group="Accessibility">
  <Prop name="label" type="string" defaultValue='"Previous slide"'>
    Accessible label announced by assistive technology.
  </Prop>
</PropsTable>

<PropsTable group="Styling">
  <Prop name="class" type="class" defaultValue="ome-carousel-prev-button-default">
    CSS class names applied to the button element.
  </Prop>
</PropsTable>

#### Fallback Icon

When the button slot is empty, a default left-arrow SVG icon is rendered. Provide slot content only when you want custom button UI (text, icon, or both).

#### Boundary Behavior

At the first slide, the previous button receives `data-ome-carousel-nav-disabled` when looping is off. With `settings.loop="true"`, the carousel wraps instead of disabling the button.

---

### Carousel Next Button

An external `<button>` element that navigates a targeted carousel forward. Identical behavior to the Previous Button but in the opposite direction. When the slot is empty, a default right-arrow SVG icon is rendered automatically.

<PropsTable group="Targeting">
  <Prop name="for" type="string" defaultValue='""'>
    Target carousel ID. This must match `identity.id` on the carousel root you want to control.
  </Prop>
</PropsTable>

<PropsTable group="Accessibility">
  <Prop name="label" type="string" defaultValue='"Next slide"'>
    Accessible label announced by assistive technology.
  </Prop>
</PropsTable>

<PropsTable group="Styling">
  <Prop name="class" type="class" defaultValue="ome-carousel-next-button-default">
    CSS class names applied to the button element.
  </Prop>
</PropsTable>

#### Fallback Icon

When the button slot is empty, a default right-arrow SVG icon is rendered. Provide slot content only when you want custom button UI.

#### Boundary Behavior

At the last slide, the next button receives `data-ome-carousel-nav-disabled` when looping is off. With `settings.loop="true"`, the carousel wraps instead of disabling the button.

---

### Carousel Dots

External pagination dots that render one clickable bullet per slide. The dots container can be placed anywhere on the page and targets a carousel by ID. At runtime, the template dot markup is extracted from the component and used to generate the correct number of bullets dynamically.

<PropsTable group="Targeting">
  <Prop name="for" type="string" defaultValue='""'>
    Target carousel ID. This must match `identity.id` on the carousel root.
  </Prop>
</PropsTable>

<PropsTable group="Styling">
  <Prop name="class" type="class" defaultValue="ome-carousel-dots-default">
    CSS class names applied to the dots wrapper element.
  </Prop>
  <Prop name="dotClass" type="class" defaultValue="ome-carousel-dot-default">
    CSS class names applied to each generated dot button. The active dot also receives `ome-carousel-dot-active` and `swiper-pagination-bullet-active`.
  </Prop>
</PropsTable>

#### Dot Generation

On initialization, the dots component extracts a hidden template button (`data-ome-carousel-dot-template`) and uses it to generate the correct number of bullets via Swiper's `renderBullet` callback. Each bullet is a `<button>` with `aria-label="Go to slide N"`. The active bullet receives the `ome-carousel-dot-active` class in addition to Swiper's default active class.

---

## CSS Custom Properties

### Navigation Buttons

```css
/* Disabled state */
[data-ome-carousel-prev][data-ome-carousel-nav-disabled],
[data-ome-carousel-next][data-ome-carousel-nav-disabled] {
  cursor: not-allowed;
  opacity: 0.5;
}
```

Navigation buttons receive `data-ome-carousel-nav-disabled` when the carousel is at the boundary in non-loop mode. Use this attribute to style the disabled state.

### Pagination Dots

```css
.ome-carousel-dots-default {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.ome-carousel-dot-default {
  width: 0.75rem;
  height: 0.75rem;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: currentColor;
  opacity: 0.35;
  cursor: pointer;
}

.ome-carousel-dot-default.ome-carousel-dot-active,
.ome-carousel-dot-default.swiper-pagination-bullet-active {
  opacity: 1;
}
```

### Carousel Root

```css
[data-ome-carousel-root] {
  width: 100%;
  position: relative;
  overflow: hidden;
}

[data-ome-carousel-root][data-ome-carousel-initialized] {
  touch-action: pan-y;
}
```

The carousel root always receives `data-ome-carousel-root`. After initialization, `data-ome-carousel-initialized` is added and `touch-action: pan-y` is applied for swipe support.

---

## Common Mistakes

<CommonMistake title="Forgetting to set a Carousel ID for external controls">
  External buttons and dots target a carousel by ID. If `identity.id` is empty on the root, previous buttons, next buttons, and dots cannot connect. The Etch editor shows a warning callout when the ID is missing.
</CommonMistake>

<CommonMistake title="Placing content directly inside Carousel without Carousel Slide">
  Only `Carousel Slide` children become navigable slides. Putting raw HTML or other blocks directly inside the carousel root (without wrapping them in a slide) breaks the Swiper layout.
</CommonMistake>

<CommonMistake title="Expecting navigation buttons to stay enabled at boundaries without loop">
  In non-loop mode, the previous button disables at the first slide and the next button disables at the last slide. Enable `settings.loop="true"` if you need wrap-around navigation.
</CommonMistake>

<CommonMistake title="Trying to change the effect via custom JSON config">
  Custom JSON config can refine the selected effect's parameters but cannot activate a different effect. The `effects.effect` prop controls which effect loads. For example, setting `effect: "fade"` in the JSON has no effect when `effects.effect` is `"slide"`.
</CommonMistake>

---

## FAQs

<details>
<summary>How do I sync two carousels together?</summary>

Set the same `identity.groupId` value on both carousel roots. The runtime uses Swiper's Controller module to keep them aligned. Each carousel also needs its own unique `identity.id`. This is commonly used for a main content carousel paired with a thumbnail strip.

</details>

<details>
<summary>Can I have multiple sets of navigation buttons for the same carousel?</summary>

Yes. You can place multiple `Carousel Previous Button` and `Carousel Next Button` components on the page, all targeting the same carousel ID. The runtime updates all matching buttons when the slide changes or the carousel reaches a boundary.

</details>

<details>
<summary>How do I make slides responsive at different viewport widths?</summary>

Set `enableResponsive` to `true` and add breakpoint rows. Each row specifies a minimum viewport width (`breakpoint`), `slidesPerView`, and `spaceBetween`. For example, a breakpoint at `768` with `slidesPerView: 2` shows two slides on screens 768px and wider.

</details>

<details>
<summary>When should I use custom config mode?</summary>

Use `useCustomConfig: true` when you need Swiper behavior that the built-in settings and autoplay props don't cover — for example, custom `freeMode`, `slidesPerGroup`, or advanced `breakpoints` configurations. The custom JSON becomes the source of truth for behavior, but identity-driven wiring (navigation buttons, dots, sync groups) stays connected automatically.

</details>

<details>
<summary>Do navigation buttons and dots need to be inside the carousel?</summary>

No. `Carousel Previous Button`, `Carousel Next Button`, and `Carousel Dots` can be placed anywhere on the page. They connect to the carousel by matching their `targeting.for` prop to the carousel's `identity.id`.

</details>
