# content-carousel

Rotate slides on autopilot or on click — galleries, hero banners, walkthroughs.


```html
<section>
  <content-carousel auto-play="1.5">
    <content-carousel-slide is-active><h2>One</h2></content-carousel-slide>
    <content-carousel-slide><h2>Two</h2></content-carousel-slide>
    <content-carousel-slide><h2>Three</h2></content-carousel-slide>
  </content-carousel>
</section>
```


## Features

- **Auto-play** Rotate on a timer via `auto-play`
- **Manual nav** `--back` / `--next` commands from plain buttons; `rel="prev"` / `rel="next"` positions them
- **Slide or fade** Choose the transition with `slide-animation`: `slide` / `fade` / `track`
- **Swipeable** `slide-animation="track"` wrapped in [`gesture-handler`](/packages/gesture-handler): drag and flick between slides
- **Pauses itself** Manual navigation stops auto-play automatically
- **Bindable position** `.provision` is `{ index, count, lastMove }` — a
  progress readout is one Quark rule on `prop("provision")`

## Installation


`@excom/content-carousel` v0.1.4

```bash
pnpm add @excom/content-carousel
```

```bash
npm install @excom/content-carousel
```

```bash
yarn add @excom/content-carousel
```

### Import

```ts
import "@excom/content-carousel";
```



## Usage

Wrap `<content-carousel-slide>` elements in a `<content-carousel>`. Mark one
`is-active`, or leave it unset to default to the first.

```html
<content-carousel auto-play="5">
  <content-carousel-slide is-active>One</content-carousel-slide>
  <content-carousel-slide>Two</content-carousel-slide>
  <content-carousel-slide>Three</content-carousel-slide>
</content-carousel>
```

`.provision` is `{ index, count, lastMove }` — set on connect and after every slide change (a command, auto-play, a slide added / removed, `is-active` written on a slide), counting only this carousel's own slides. A "2 / 3" readout:

```quark
content-carousel {
  $slide: prop("provision").index + 1;
  $count: prop("provision").count;
  [bind-progress] { content: "#{$slide} / #{$count}"; }
}
```

Nothing animates until the first move: `last-move` is unset until then, so an initial `is-active`, in markup or written by the app, appears in place. `slide-animation="track"` renders only the active slide and its two DOM neighbours, so slides must be siblings (a `display: contents` wrapper around them is fine) and, with three or more slides, a wrap — `--next` from the last, `--back` from the first — cuts to the new slide instead of sliding; put it inside [`gesture-handler`](/packages/gesture-handler) to drag it.

### API Reference


#### Attributes

| Name | Surface | Type | Default | Values | Description |
| --- | --- | --- | --- | --- | --- |
| `auto-play` | option | `number` |  |  | Seconds between automatic slide advances. Unset disables auto-play. |
| `is-scrubbing` | option | `boolean` |  |  | Set when the element is being dragged. Used by CSS only to disable `transition`. |
| `slide-animation` | option | `string` | `"slide"` | `"slide"` \| `"fade"` \| `"track"` | Transition used when the active slide changes. |
| `last-move` | state | `string` |  | `"forward"` \| `"back"` | Direction of the most recent slide change — drives the CSS animation direction. `is-active` moved from one slide to another counts as `forward` / `back` by position. Unset until the first move, so the initial slide appears without animating in. |
| `auto-play-stopped` | hybrid | `boolean` |  |  | Set automatically after manual navigation, or when the element leaves the document (a DOM move keeps auto-play running), to pause auto-play. Set it directly to pause / resume auto-play programmatically. |

#### Provision

| Name | Type | Description |
| --- | --- | --- |
| `provision` | `ContentCarouselProvision` (`{ index: number; count: number; lastMove: "forward" \| "back" \| null; }`) | Position: `{ index, count, lastMove }` — the active slide's index among this carousel's own slides, how many there are, and the direction of the last move. Set on connect and after every change, including slides added / removed and `is-active` written on a slide. Not reflected as an attribute. |

#### Recognized Elements

| Relationship | Selector | Required | Description |
| --- | --- | --- | --- |
| `content-carousel-slide` | descendant | yes | Slides to rotate through. The first, or whichever has `is-active`, is shown initially. |

#### Fires

| Name | Type | Description |
| --- | --- | --- |
| `content-carousel-slide-changed` | `ContentCarouselSlideChangedEvent` (`CustomEvent & { type: "content-carousel-slide-changed"; detail: { activeSlide: HTMLElement \| null; previousSlide: HTMLElement \| null; }; bubbles: true; cancelable: true; composed: true }`) | After any slide change: a command, auto-play, or `is-active` moved from one slide to another (a swipe). |

#### Commands

| Command | Action |
| --- | --- |
| `--back` | Shows the previous slide (wraps to last). Stops auto-play. |
| `--next` | Shows the next slide (wraps to first). Stops auto-play. |

#### CSS Custom Properties

| Name | Syntax | Default | Description |
| --- | --- | --- | --- |
| `--content-carousel-slide-animation-duration` | `<time>` | `0.25s` | Length of the slide / fade transition. |
| `--content-carousel-transition-ease` | `<easing-function>` | `ease-out` | Easing used for the fade transition. |
| `--content-carousel-button-size` | `<length>` | `25px` | Min-width of the nav buttons. |
| `--content-carousel-progress` | `<number>` | `0` | `slide-animation="track"` only: how far the track is dragged, in slide widths — `1` shows the next slide, `-1` the previous. While `is-scrubbing` it follows a wrapping `<gesture-handler>`'s inherited `--gesture-progress` unless set explicitly — swipeable carousels. |

#### CSS Classes

| Name | Description |
| --- | --- |
| `.tag-content-carousel-prev` | Positions a direct child with `rel="prev"` (or `.tag-content-carousel-prev`) as the "previous slide" nav control. |
| `.tag-content-carousel-next` | Positions a direct child with `rel="next"` (or `.tag-content-carousel-next`) as the "next slide" nav control. |

#### CSS Aliases

| Alias | Kind | Matches | Description |
| --- | --- | --- | --- |
| `:--content-carousel` | element | `content-carousel`, `.tag-content-carousel` |  |
| `:--content-carousel--last-move` | state | `[last-move]`, `[data-last-move]` |  |
| `:--content-carousel--last-move-forward` | state | `[last-move="forward"]`, `[data-last-move="forward"]` |  |
| `:--content-carousel--last-move-back` | state | `[last-move="back"]`, `[data-last-move="back"]` |  |
| `:--content-carousel--slide-animation` | state | `[slide-animation]`, `[data-slide-animation]` |  |
| `:--content-carousel--slide-animation-slide` | state | `[slide-animation="slide"]`, `[data-slide-animation="slide"]` |  |
| `:--content-carousel--slide-animation-fade` | state | `[slide-animation="fade"]`, `[data-slide-animation="fade"]` |  |
| `:--content-carousel--slide-animation-track` | state | `[slide-animation="track"]`, `[data-slide-animation="track"]` |  |
| `:--content-carousel--is-scrubbing` | state | `[is-scrubbing]`, `[data-scrubbing]` |  |



### Examples

#### Manual navigation

Invoke `--back` / `--next` from a `<button command commandfor>`. A direct
child with `rel="prev"` / `rel="next"` (or
`.tag-content-carousel-prev` / `.tag-content-carousel-next`) is
positioned as the nav control for you. If you want swipeable slides, you
must use the carousel in conjunction with the [<gesture-handler>](/packages/gesture-handler).
A demo exists on that page.


```html
<section>
  <content-carousel id="demo-carousel-manual">
    <button type="button" rel="prev" command="--back" commandfor="demo-carousel-manual">‹</button>
    <button type="button" rel="next" command="--next" commandfor="demo-carousel-manual">›</button>
    <content-carousel-slide is-active><h2>One</h2></content-carousel-slide>
    <content-carousel-slide><h2>Two</h2></content-carousel-slide>
    <content-carousel-slide><h2>Three</h2></content-carousel-slide>
  </content-carousel>
</section>
```


#### Fade transition

`slide-animation="fade"` crossfades instead of sliding.


```html
<section>
  <content-carousel auto-play="1.5" slide-animation="fade">
    <content-carousel-slide is-active><h2>One</h2></content-carousel-slide>
    <content-carousel-slide><h2>Two</h2></content-carousel-slide>
    <content-carousel-slide><h2>Three</h2></content-carousel-slide>
  </content-carousel>
</section>
```

## Release notes

### 0.1.3 (2026-10-01)

- Track mode hides every slide beyond the active slide's neighbours and no longer animates the first slide into place

### 0.1.2 (2026-09-30)

- Auto-play tests poll for the advanced slide instead of sleeping a fixed 80 ms (flaked on loaded CI runners); no runtime change
- Fix an issue where `provision` did not follow slides added or removed, and `last-move` and `content-carousel-slide-changed` did not follow `is-active` moving from one slide to another (a swipe): each change sets one provision, and a carousel moved in the DOM no longer sets it again
- Update the internal `autoPlayIntervalId` property to `_autoPlayIntervalId`, as its other private properties are named; it was never an attribute, so markup is unaffected

### 0.1.1 (2026-09-23)

- Ship only dist (and declared extras) in the npm tarball; drop build logs, tests and sources
