---
title: Chapters
description: A scroll-driven page layout that highlights the active chapter in a sticky sidebar as the user scrolls, with theme support for brand ranges
---

```tsx
'use client';

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

export function ChaptersDemo() {
  return (
    <Chapters
      chapters={[
        {
          title: 'All-inclusive experience',
          content: (
            <div className="min-h-screen p-40">
              <p className="text-body max-w-prose">
                From sunrise yoga to late-night entertainment, every moment is included. Fine
                dining, premium beverages, world-class facilities — yours without a second thought.
              </p>
            </div>
          ),
        },
        {
          title: 'Sports & activities',
          content: (
            <div className="min-h-screen p-40">
              <p className="text-body max-w-prose">
                Surf, dive, sail, climb — over 60 land and water sports await you. Our certified
                instructors guide guests of all levels, from first-timers to seasoned athletes.
              </p>
            </div>
          ),
        },
        {
          title: 'Kids clubs',
          content: (
            <div className="min-h-screen p-40">
              <p className="text-body max-w-prose">
                Dedicated clubs for every age from 4 months to 17 years. Supervised activities,
                creative workshops, and adventure programmes tailored to each stage of childhood.
              </p>
            </div>
          ),
        },
      ]}
      theme="coreRange"
      className="w-full"
    />
  );
}
```

## Installation

Using the CLI:

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

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

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

import type { IconicNames } from '@clubmed/trident-icons';
import type { Theme } from '../types/Theme';

import { Chapter } from './Chapter';
import { ChapterTitle } from './ChapterTitle';
import { ChaptersSummary } from './ChaptersSummary';

export interface ChapterItem {
  content: ReactNode;
  title: string;
}

export interface ChaptersProps
  extends Omit<ComponentPropsWithoutRef<'section'>, 'onClick' | 'children'> {
  chapters: ChapterItem[];
  hLevel?: 2 | 3 | 4;
  icon?: IconicNames;
  onClick?: (chapter: ChapterItem) => void;
  theme?: Theme;
}

export const Chapters: FunctionComponent<ChaptersProps> = ({
  chapters,
  className,
  hLevel = 2,
  icon,
  onClick,
  theme = 'coreRange',
  ...attrs
}) => {
  const [activeChapter, setActiveChapter] = useState(0);

  return (
    <section {...attrs} className={clsx(className, 'flex @container/chapters')}>
      <ChaptersSummary
        activeChapter={activeChapter}
        chapters={chapters}
        className="hidden w-3/10 @md:block @xl:w-[calc(50%-380px)]"
        icon={icon}
        onClick={onClick}
        theme={theme}
      />

      <div
        className={clsx(
          'relative isolate w-full flex-1 @md:max-w-7/10 @xl:max-w-[calc(50%+380px)]',
          '@md:ps-20 @lg:ps-40 @xl:ps-120',
        )}
      >
        {chapters.map((chapter, index) => (
          <Chapter index={index} key={chapter.title} setActiveChapter={setActiveChapter}>
            <ChapterTitle
              hasNext={index < chapters.length - 1}
              hLevel={hLevel}
              index={index}
              isActive={index === activeChapter}
              onClick={() => onClick?.(chapter)}
              theme={theme}
            >
              {chapter.title}
            </ChapterTitle>
            {chapter.content}
          </Chapter>
        ))}
      </div>
    </section>
  );
};
```

## Usage

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

export function Component() {
  return (
    <Chapters
      chapters={[
        {
          title: 'All-inclusive experience',
          content: <section className="min-h-screen p-40">…</section>,
        },
        {
          title: 'Sports & activities',
          content: <section className="min-h-screen p-40">…</section>,
        },
      ]}
      theme="coreRange"
    />
  );
}
```

## Anatomy

`Chapters` is composed of three internal sub-components:

| Sub-component      | Role                                                                          |
| ------------------ | ----------------------------------------------------------------------------- |
| `ChaptersSummary`  | Sticky sidebar `<nav>` with one link per chapter; hidden on mobile            |
| `Chapter`          | Wrapper `<section>` that observes its own intersection to report active state |
| `ChapterTitle`     | Sticky mobile heading that anchors each chapter section                       |

## API Reference

### Chapters

| Prop        | Type              | Default       | Description                                                               |
| ----------- | ----------------- | ------------- | ------------------------------------------------------------------------- |
| `chapters`  | `ChapterItem[]`   | Required      | Array of chapter objects; each has a `title` and arbitrary `content`      |
| `theme`     | `Theme`           | `'coreRange'` | Visual theme — controls sidebar and title background/text colours          |
| `hLevel`    | `2 \| 3 \| 4`    | `2`           | Heading level for each chapter title (`<h2>` by default)                  |
| `icon`      | `IconicNames`     | `undefined`   | Override the default brand icon shown at the bottom of the sidebar        |
| `onClick`   | `(chapter: ChapterItem) => void` | `undefined` | Called when a chapter title or sidebar link is clicked |
| `className` | `string`          | `undefined`   | Additional CSS classes on the root `<section>`                            |

### ChapterItem

| Field     | Type        | Description                              |
| --------- | ----------- | ---------------------------------------- |
| `title`   | `string`    | Chapter heading text and sidebar label   |
| `content` | `ReactNode` | Arbitrary content rendered below the title |

## Themes

The `theme` prop switches the sidebar and mobile title bar colours. Supported values:

| Value                 | Background     | Text  | Icon               |
| --------------------- | -------------- | ----- | ------------------ |
| `coreRange` (default) | `lightSand`    | black | Trident            |
| `exclusiveCollection` | `ultramarine`  | white | ExclusiveCollection |
| `happyToCare`         | `verdigris`    | black | HappyToCare        |
| `sienna`              | `sienna`       | white | Trident            |
| `black`               | `black`        | white | Trident            |
| `white`               | `white`        | black | Trident            |
| `light`               | `lightSand`    | black | Trident            |
| `dark`                | `ultramarine`  | white | ExclusiveCollection |

## Behaviour

### Scroll-driven active chapter

Each `Chapter` section registers an `IntersectionObserver` against its root element. The active
chapter index is determined by which section satisfies one of three visibility rules:

1. Section height ≤ half the viewport **and** intersection height > one-third the viewport
2. Section height ≤ half the viewport **and** section is fully visible
3. Section height > half the viewport **and** intersection height > half the viewport

The matching chapter's sidebar link is displayed at full opacity; inactive links are shown at 50 %.

### Mobile sticky titles

On mobile, each chapter title sticks to the top of the viewport as the user scrolls through that
section, allowing orientation without an always-visible sidebar.

### Anchor-based navigation

Sidebar links and the "next chapter" arrow anchor to `#chapter{index}`, enabling in-page keyboard
navigation and shareable deep links.

## Accessibility

- The sidebar is a `<nav>` landmark; screen readers announce it as navigation.
- Each chapter is a `<section>` element — equivalent to a `region` landmark when given an
  accessible name via its `id`.
- Chapter titles are rendered as `<h2>` (configurable via `hLevel`), providing document outline
  structure for assistive technology.
- The "next chapter" arrow on mobile is an `<a>` element targeting the subsequent chapter anchor.
- The sidebar brand icon is purely decorative (no accessible name required).

## Notes

- All four files (`Chapters`, `Chapter`, `ChapterTitle`, `ChaptersSummary`) are marked
  `'use client'` — they depend on `useState`, `useEffect`, and browser APIs.
- `IntersectionObserver` is used directly; it is not available in JSDOM. Tests must provide a
  stub — see the spec file for the pattern.
- The sidebar is hidden on mobile (`hidden md:block`) and shown at `md` breakpoint and above.
  On mobile, `ChapterTitle` provides equivalent navigation via sticky headings and prev/next anchors.
- The optional `icon` prop accepts any `IconicNames` value from `@clubmed/trident-icons`. When
  omitted, the theme's default icon is used (usually `Trident`, or the brand-specific icon).
