---
outline: deep
---

# Stories

Instagram-style web stories. A horizontal row of clickable thumbnails opens a fullscreen viewer that plays each video with a segmented progress bar, previous/next navigation, mute toggle, and auto-advance.

**`<l-stories>`** — Custom Element (no Shadow DOM)

## Options

### Rounded stories

The default appearance — circular thumbnails with a label below. Videos open in full screen.

```html
<l-stories for="stories-rounded">
  <l-story
    src="https://res.cloudinary.com/demo/video/upload/c_fill,ar_9:16,h_1280,q_auto/dog.mp4"
    poster="https://res.cloudinary.com/demo/video/upload/c_fill,ar_9:16,h_240,q_auto/dog.jpg"
    label="Concept"
    pulse
  ></l-story>
  <l-story
    src="https://res.cloudinary.com/demo/video/upload/c_fill,ar_9:16,h_1280,q_auto/sea_turtle.mp4"
    poster="https://res.cloudinary.com/demo/video/upload/c_fill,ar_9:16,h_240,q_auto/sea_turtle.jpg"
    preview="https://res.cloudinary.com/demo/video/upload/c_fill,ar_9:16,h_400,q_auto/sea_turtle.mp4"
    label="Origin"
  ></l-story>
  <l-story
    src="https://res.cloudinary.com/demo/video/upload/c_fill,ar_9:16,h_1280,q_auto/elephants.mp4"
    poster="https://res.cloudinary.com/demo/video/upload/c_fill,ar_9:16,h_240,q_auto/elephants.jpg"
    label="Crafted"
  ></l-story>
  <l-story
    src="https://res.cloudinary.com/demo/video/upload/c_fill,ar_9:16,h_1280,q_auto/kitten_fighting.mp4"
    poster="https://res.cloudinary.com/demo/video/upload/c_fill,ar_9:16,h_240,q_auto/kitten_fighting.jpg"
    label="Made by hand"
  ></l-story>
  <l-story
    src="https://res.cloudinary.com/demo/video/upload/c_fill,ar_9:16,h_1280,q_auto/big_buck_bunny.mp4"
    poster="https://res.cloudinary.com/demo/video/upload/c_fill,ar_9:16,h_240,q_auto/big_buck_bunny.jpg"
    label="Behind the scenes"
  ></l-story>
</l-stories>

<l-stories-viewer id="stories-rounded"></l-stories-viewer>
```

The thumbnail ring picks up `--ring-color` (any `background` value — solid, `linear-gradient`, `conic-gradient`, image) and shows a `--ring-offset` gap filled with `--ring-offset-color`. Pair a thicker `--ring-width` with the gap to make fresh stories stand out:

```html
<l-stories
  for="stories-gradient"
  style="
    --ring-color: linear-gradient(
      45deg,
      #f09433 0%,
      #e6683c 25%,
      #dc2743 50%,
      #cc2366 75%,
      #bc1888 100%
    );
    --ring-width: 3px;
    --ring-offset: 3px;
  "
>
  <l-story
    src="https://res.cloudinary.com/demo/video/upload/c_fill,ar_9:16,h_1280,q_auto/dog.mp4"
    poster="https://res.cloudinary.com/demo/video/upload/c_fill,ar_9:16,h_240,q_auto/dog.jpg"
    label="Anna"
    pulse
  ></l-story>
  <l-story
    src="https://res.cloudinary.com/demo/video/upload/c_fill,ar_9:16,h_1280,q_auto/sea_turtle.mp4"
    poster="https://res.cloudinary.com/demo/video/upload/c_fill,ar_9:16,h_240,q_auto/sea_turtle.jpg"
    label="Bruno"
  ></l-story>
  <l-story
    src="https://res.cloudinary.com/demo/video/upload/c_fill,ar_9:16,h_1280,q_auto/elephants.mp4"
    poster="https://res.cloudinary.com/demo/video/upload/c_fill,ar_9:16,h_240,q_auto/elephants.jpg"
    label="Claire"
  ></l-story>
</l-stories>

<l-stories-viewer id="stories-gradient"></l-stories-viewer>
```

### Squared stories

Set `appearance="squared"` for square thumbnails with rounded corners. Videos open in full screen.

```html
<l-stories
  for="stories-squared"
  appearance="squared"
>
  <l-story
    src="https://res.cloudinary.com/demo/video/upload/c_fill,ar_9:16,h_1280,q_auto/dog.mp4"
    poster="https://res.cloudinary.com/demo/video/upload/c_fill,ar_1:1,h_400,q_auto/dog.jpg"
    label="The product"
  ></l-story>
  <l-story
    src="https://res.cloudinary.com/demo/video/upload/c_fill,ar_9:16,h_1280,q_auto/sea_turtle.mp4"
    poster="https://res.cloudinary.com/demo/video/upload/c_fill,ar_1:1,h_400,q_auto/sea_turtle.jpg"
    label="How it's made"
  ></l-story>
  <l-story
    src="https://res.cloudinary.com/demo/video/upload/c_fill,ar_9:16,h_1280,q_auto/elephants.mp4"
    poster="https://res.cloudinary.com/demo/video/upload/c_fill,ar_1:1,h_400,q_auto/elephants.jpg"
    label="About us"
  ></l-story>
</l-stories>

<l-stories-viewer id="stories-squared"></l-stories-viewer>
```

### Portrait stories

Set `appearance="portrait"` for tall video cards (9:16). Videos open in full screen.

```html
<l-stories
  for="stories-portrait"
  appearance="portrait"
>
  <l-story
    src="https://res.cloudinary.com/demo/video/upload/c_fill,ar_9:16,h_1280,q_auto/dog.mp4"
    poster="https://res.cloudinary.com/demo/video/upload/c_fill,ar_9:16,h_640,q_auto/dog.jpg"
    label="Look #01"
  ></l-story>
  <l-story
    src="https://res.cloudinary.com/demo/video/upload/c_fill,ar_9:16,h_1280,q_auto/sea_turtle.mp4"
    poster="https://res.cloudinary.com/demo/video/upload/c_fill,ar_9:16,h_640,q_auto/sea_turtle.jpg"
    label="Look #02"
  ></l-story>
  <l-story
    src="https://res.cloudinary.com/demo/video/upload/c_fill,ar_9:16,h_1280,q_auto/elephants.mp4"
    poster="https://res.cloudinary.com/demo/video/upload/c_fill,ar_9:16,h_640,q_auto/elephants.jpg"
    label="Look #03"
  ></l-story>
</l-stories>

<l-stories-viewer id="stories-portrait"></l-stories-viewer>
```

### Chapters

Split a single video into chapters with `chapters="0,5,12,20"` (start times in seconds, comma-separated; `0` is implicit). The progress bar renders one segment per chapter; tap or `→` advances to the next chapter, then auto-advances to the next story at the last chapter.

```html
<l-stories for="stories-chapters">
  <l-story
    src="https://res.cloudinary.com/demo/video/upload/c_fill,ar_9:16,h_1280,q_auto/big_buck_bunny.mp4"
    poster="https://res.cloudinary.com/demo/video/upload/c_fill,ar_9:16,h_240,q_auto/big_buck_bunny.jpg"
    label="Reviews"
    chapters="0,4,8"
  ></l-story>
  <l-story
    src="https://res.cloudinary.com/demo/video/upload/c_fill,ar_9:16,h_1280,q_auto/elephants.mp4"
    poster="https://res.cloudinary.com/demo/video/upload/c_fill,ar_9:16,h_240,q_auto/elephants.jpg"
    label="How it's made"
  ></l-story>
  <l-story
    src="https://res.cloudinary.com/demo/video/upload/c_fill,ar_9:16,h_1280,q_auto/kitten_fighting.mp4"
    poster="https://res.cloudinary.com/demo/video/upload/c_fill,ar_9:16,h_240,q_auto/kitten_fighting.jpg"
    label="Concept"
  ></l-story>
</l-stories>

<l-stories-viewer id="stories-chapters"></l-stories-viewer>
```

### Shoppable overlay

Slot a CTA into `<l-story slot="cta">`. The viewer surfaces it only on the active story.

```html
<l-stories for="stories-shop">
  <l-story
    src="https://res.cloudinary.com/demo/video/upload/c_fill,ar_9:16,h_1280,q_auto/dog.mp4"
    poster="https://res.cloudinary.com/demo/video/upload/c_fill,ar_9:16,h_240,q_auto/dog.jpg"
    label="Best seller"
  >
    <a
      slot="cta"
      href="#"
      class="l-button"
      data-variant="primary"
    >
      Shop the look — 49€
    </a>
  </l-story>
  <l-story
    src="https://res.cloudinary.com/demo/video/upload/c_fill,ar_9:16,h_1280,q_auto/sea_turtle.mp4"
    poster="https://res.cloudinary.com/demo/video/upload/c_fill,ar_9:16,h_240,q_auto/sea_turtle.jpg"
    label="New drop"
  >
    <a
      slot="cta"
      href="#"
      class="l-button"
      data-variant="primary"
    >
      Shop the drop — 79€
    </a>
  </l-story>
</l-stories>

<l-stories-viewer id="stories-shop"></l-stories-viewer>
```

## Accessibility

### Criteria

- **Roles** — Row is a `list` of triggers; viewer is a native modal `<dialog>`; progress bar has `role="progressbar"`
- **Accessible name** — Each thumbnail uses `label` as its `aria-label`; viewer announces "Story X of N — label" on change
- **Focus management** — Native dialog traps focus; closes restore focus to the originating thumbnail
- **Autoplay policy** — Video opens muted by default — `m` or the mute toggle requires a user gesture to unmute
- **Motion** — Respects `prefers-reduced-motion` for transitions and progress fill smoothing

### Rules

- Always provide a `label` on every `l-story` — it becomes the trigger `aria-label`
- Pair the row and viewer with matching `for` ↔ `id` so multiple rows can share one viewer
- Provide poster images so the row renders without loading any video

### Keyboard interactions

- `Enter` — Opens the viewer at the focused thumbnail
- `Arrow Left / Right` — Previous / next chapter (crosses into the previous / next story at the chapter boundary)
- `Space` — Toggles play/pause
- `M` — Toggles mute
- `Escape` — Closes the viewer

## API reference

### Importing

```js
import 'luxen-ui/stories';
import 'luxen-ui/story';
import 'luxen-ui/stories-viewer';
```

```css
@import 'luxen-ui/css/stories';
@import 'luxen-ui/css/story';
```

### Attributes & Properties

- **for**: `string` — ID of the linked `<l-stories-viewer>`.
- **appearance**: `StoriesAppearance` (default: `'rounded'`) — Visual appearance of the thumbnails.

To customize size, radius, gap, or hide labels, set the [CSS custom properties](#css-custom-properties) directly via inline `style` or external CSS — no extra attributes needed.

### Methods

- **open(index: unknown)** — Open the linked viewer at the given index.
- **stories()** → `LuxenStory[]` — All `<l-story>` children.

### Events

- **story-open** — Fired when the viewer opens. Properties: `index: number`, `story: LuxenStory`.

### CSS custom properties

These tokens are set on `<l-stories>` and cascade to every `<l-story>` child.

- `--size` — Thumbnail size (width). Default per appearance.
- `--radius` — Thumbnail border radius. Default per appearance (`--radius-full` for rounded, `1rem` for portrait).
- `--gap` (default: `1rem`) — Gap between thumbnails.
- `--ring-color` — Ring color around fresh thumbnails.
- `--ring-color-seen` — Ring color for `[seen]` thumbnails.
- `--ring-width` (default: `2px`) — Ring width.
- `--label-color` — Label text color.

The play-icon disc inside `.l-story-play l-icon` is styled inline (white icon, 35 % black background). Override it directly via standard CSS: `l-story .l-story-play l-icon { background: …; color: … }`.
