Chapter Rail

The chapter list that docks at the side once the reader scrolls past the Story Cover, and lights the chapter being read.

I

The bean

Washed coffee from one farm in Huila, roasted light on Tuesdays so it tastes of the fruit, not the fire.

II

The pour

Sixteen grams to two hundred and fifty of water, poured in three slow circles over three minutes.

III

The cup

Thin porcelain, warmed first, so the first sip is as bright as the last.

import { ChapterRail } from '@brand-studio/ui';

const chapters = [
  { id: 'rail-bean', title: 'The bean', marker: 'I', body: 'Washed coffee from one farm in Huila, roasted light on Tuesdays so it tastes of the fruit, not the fire.' },
  { id: 'rail-pour', title: 'The pour', marker: 'II', body: 'Sixteen grams to two hundred and fifty of water, poured in three slow circles over three minutes.' },
  { id: 'rail-cup', title: 'The cup', marker: 'III', body: 'Thin porcelain, warmed first, so the first sip is as bright as the last.' },
];

export default function ChapterRailDemo() {
  return (
    <div style={{ display: 'grid', gridTemplateColumns: 'minmax(8rem, 11rem) 1fr', gap: 32, width: '100%', padding: 32 }}>
      <div style={{ position: 'sticky', top: 96, alignSelf: 'start' }}>
        <ChapterRail placement="inline" title="Still Coffee" chapters={chapters.map(c => ({ label: c.title, href: `#${c.id}`, marker: c.marker }))} />
      </div>
      <div style={{ display: 'grid', gap: 48 }}>
        {chapters.map(c => <section key={c.id} id={c.id} style={{ minHeight: 280 }}>
          <p style={{ margin: 0, opacity: .6 }}>{c.marker}</p>
          <h3 style={{ margin: '4px 0 8px', fontSize: 28 }}>{c.title}</h3>
          <p style={{ margin: 0, maxWidth: '40ch', opacity: .8 }}>{c.body}</p>
        </section>)}
      </div>
    </div>
  );
}

Installation

pnpm add @brand-studio/ui

Usage

import { ChapterRail, StoryCover } from '@brand-studio/ui';
<StoryCover id="cover" title="Three plates" chapters={chapters} />
<ChapterRail cover="cover" chapters={chapters} />

By default the rail is fixed to the left edge of wide screens. It appears once the first chapter reaches the middle of the screen, or a quarter of the cover has scrolled away, and leaves once the last chapter ends. Where the browser supports view transitions, the cover's chapter list moves into the rail. Leave about 11rem free at the left of wide screens so the rail does not cover your text.

With placement="inline" the rail stays where you put it, such as a sticky side column, and only lights the current chapter. The preview above works this way.

Accessibility

  • The rail is a <nav> named by label, with an ordered list of links.
  • The chapter being read has aria-current, so screen readers announce it.
  • The fixed rail is hidden on narrow screens and before it docks, so readers meet one chapter list at a time.
  • With reduced motion the list appears without moving.

API Reference

PropTypeDefault
chaptersStoryCoverChapter[]
cover?string
title?React.ReactNode
placement?"fixed" | "inline"'fixed'
label?string'Chapters'
className?string''

Still, Deskhand and Hollis are fictional brands. The coffee photographs are original generated artwork. @brand-studio/ui is MIT licensed.