---
title: Promises
description: An auto-advancing carousel showcasing brand promises, with a dot-progress indicator, previous/next navigation, and a mobile overlay menu for item selection
---

```tsx
'use client';

import { Promises } from '@/ui/Promises/Promises';

export function PromisesDemo() {
  return (
    <Promises
      className="min-h-500 w-full"
      items={[
        {
          title: 'The most beautiful destinations',
          href: '#',
          image: {
            src: 'https://images.unsplash.com/photo-1507525428034-b723cf961d3e?w=400',
            alt: 'Beautiful beach destination',
          },
        },
        {
          title: 'Kids club starting from 4 months',
          href: '#',
          image: {
            src: 'https://images.unsplash.com/photo-1529156069898-49953e39b3ac?w=400',
            alt: 'Kids playing at the club',
          },
        },
        {
          title: 'The best restaurants on the coast',
          href: '#',
          image: {
            src: 'https://images.unsplash.com/photo-1414235077428-338989a2e8c0?w=400',
            alt: 'Fine dining restaurant',
          },
        },
      ]}
      labels={{
        anchor: { continue: 'Discover' },
        layer: { close: 'Close menu', open: 'Open menu' },
        scroller: { previous: 'Previous', next: 'Next' },
      }}
    />
  );
}
```

## Installation

Using the CLI:

```bash
npx shadcn@latest add https://develop.trident-ui.pro.clubmed/r/promises.json
```

Or manually, copy the following code into `/ui/Promises/Promises.tsx`:

```tsx
'use client';
import clsx from 'clsx';
import type { ComponentPropsWithoutRef, FunctionComponent } from 'react';
import { useCallback, useEffect, useState } from 'react';

import type { ImageProps } from '../Image';

import { PromisesMedia } from './PromisesMedia';
import { PromisesMenu } from './PromisesMenu';

const INTERVAL_IN_MS = 10000;

export interface PromisesItem {
  href: string;
  id?: string;
  image: ImageProps;
  title: string;
}

export interface PromisesLabels {
  anchor: { continue: string };
  layer: { close: string; open: string };
  scroller: { previous: string; next: string };
}

export interface PromisesProps extends ComponentPropsWithoutRef<'div'> {
  items: PromisesItem[];
  labels: PromisesLabels;
}

export const Promises: FunctionComponent<PromisesProps> = ({
  className,
  items,
  labels,
  ...attrs
}) => {
  const [activeIndex, setActiveIndex] = useState(0);
  const [intervalKey, setIntervalKey] = useState(0);

  const previousActiveIndex = useCallback(() => {
    setActiveIndex((state) => (state - 1 + items.length) % items.length);
  }, [items.length]);

  const nextActiveIndex = useCallback(() => {
    setActiveIndex((state) => (state + 1) % items.length);
  }, [items.length]);

  const goToIndex = useCallback((index: number) => {
    setActiveIndex(index);
    setIntervalKey((k) => k + 1);
  }, []);

  useEffect(() => {
    const interval = setInterval(() => {
      nextActiveIndex();
    }, INTERVAL_IN_MS);

    return () => clearInterval(interval);
  }, [nextActiveIndex, intervalKey]);

  return (
    <div
      {...attrs}
      className={clsx('relative isolate flex gap-x-20 @container/promises', className)}
    >
      <PromisesMedia
        activeIndex={activeIndex}
        items={items}
        labels={labels}
        onClickMenu={(i) => setActiveIndex(i)}
        onNext={nextActiveIndex}
        onPrevious={previousActiveIndex}
      />
      <PromisesMenu
        activeIndex={activeIndex}
        items={items}
        labels={labels}
        onClickMenu={(i) => goToIndex(i)}
      />
    </div>
  );
};
```

## Usage

```tsx showLineNumbers
import { Promises } from '@/ui/Promises/Promises';

export function Component() {
  return (
    <Promises
      items={[
        {
          title: 'The most beautiful destinations',
          href: '/destinations',
          image: { src: '/images/destinations.jpg', alt: 'Beach destination' },
        },
        {
          title: 'Kids club starting from 4 months',
          href: '/kids-club',
          image: { src: '/images/kids.jpg', alt: 'Kids at the club' },
        },
      ]}
      labels={{
        anchor: { continue: 'Discover' },
        layer: { close: 'Close menu', open: 'Open menu' },
        scroller: { previous: 'Previous', next: 'Next' },
      }}
    />
  );
}
```

## Anatomy

`Promises` is composed of three internal sub-components that are exported for advanced use:

| Sub-component    | Role                                                                 |
| ---------------- | -------------------------------------------------------------------- |
| `PromisesMedia`  | Stacked image slides with a progress dot bar and prev/next controls  |
| `PromisesMenu`   | Title list — overlay on mobile, sidebar at `@3xl` container width    |

## API Reference

### Promises

| Prop        | Type             | Default     | Description                                                         |
| ----------- | ---------------- | ----------- | ------------------------------------------------------------------- |
| `items`     | `PromisesItem[]` | Required    | Array of carousel items (title, href, image)                        |
| `labels`    | `PromisesLabels` | Required    | Localised strings for the anchor, layer toggle, and scroller        |
| `className` | `string`         | `undefined` | Additional CSS classes applied to the root container                |

### PromisesItem

| Field   | Type         | Default     | Description                                               |
| ------- | ------------ | ----------- | --------------------------------------------------------- |
| `title` | `string`     | Required    | Title displayed on the media overlay and in the menu      |
| `href`  | `string`     | Required    | URL the "continue" anchor links to                        |
| `image` | `ImageProps` | Required    | Props forwarded to the trident-ui `Image` component       |
| `id`    | `string`     | `undefined` | Optional stable id; used for accessible labelling         |

### PromisesLabels

| Field             | Type     | Description                                      |
| ----------------- | -------- | ------------------------------------------------ |
| `anchor.continue` | `string` | Label for the "continue" anchor inside each item |
| `layer.open`      | `string` | Aria-label for the toggle button (closed state)  |
| `layer.close`     | `string` | Aria-label for the toggle button (open state)    |
| `scroller.previous` | `string` | Screen-reader label for the previous button    |
| `scroller.next`   | `string` | Screen-reader label for the next button          |

## Behaviour

### Auto-advance

Items cycle automatically every **10 seconds**. The timer resets whenever the user interacts with
the previous/next buttons or the dot indicators. The interval clears on unmount.

### Responsive layout

`Promises` uses a CSS container named `promises`. Below `@3xl` the menu is an overlay (toggle
with the up-arrow button). At `@3xl` and above the menu becomes a static sidebar and the toggle
button is hidden.

### Dot progress bar

Each dot animates a fill from left to right over 10 s (`animate-filling`) while that item is
active. Dots for past items are filled white; future items show at 40 % opacity.

## Accessibility

- Each image slide is an `<article>` with `aria-hidden` and `inert` when not active — screen
  readers and keyboard focus skip inactive slides.
- The media region carries `aria-hidden` / `inert` when the overlay menu is open, preventing
  accidental focus trap on the hidden layer.
- Previous and next buttons expose their purpose through `sr-only` text taken from
  `labels.scroller`.
- The overlay toggle button updates its `aria-label` between `labels.layer.open` and
  `labels.layer.close` to communicate the current menu state.
- Dot indicator buttons carry `aria-current` to announce the active slide to assistive technology.

## Notes

- All three files (`Promises`, `PromisesMedia`, `PromisesMenu`) are marked `'use client'` — they
  rely on `useState`, `useEffect`, and `useCallback`.
- `PromisesMedia` and `PromisesMenu` are internal sub-components; import `Promises` to consume
  the full organism.
- `@3xl/promises:` container-query modifiers require `@tailwindcss/container-queries`. The
  `@container/promises` class is added to the root `<div>` automatically.
