A narrow sticky rail on the left holds a numbered section index that highlights the active section as the user scrolls. The right side of the page carries the actual content. Quiet, navigational, scholarly.
A complete design system, ready for your AI coding agent. Every primitive, token, and pattern below is generated straight from DESIGN.md — drop the file in your project and ship matching UI in minutes.
Prose-first token file — decisions live next to their reasoning.
Fine micro-scale (1–5px) for pills, editorial scale (12–21px) for the grid.
The system's own radius tokens — sm for chips and inputs, md for buttons, lg for cards, pill for fully-rounded CTAs.
What sets a pattern apart from a system: a defined grid, divider rhythm, and section composition. Everything below uses the same tokens that power the kitchen sink — applied to a real page skeleton.
The structure does the talking. Typography, spacing, and one accent — nothing else competing for attention.
1---2name: "Sticky Index Rail"3description: "A narrow sticky rail on the left holds a numbered section index that highlights the active section as the user scrolls. The right side of the page carries the actual content. Quiet, navigational, scholarly."4tags: [layout, navigation, sticky, "long-read"]5type: pattern6container: centered7content_max_width: 1080px8page_padding: 80px9grid:10 columns: 211 max_columns: 212 line_color: transparent13 line_width: 0px14 line_style: solid15 edge_lines: false16sections:17 padding_y: 120px18 divider_color: "rgba(15, 15, 15, 0.06)"19 divider_width: 1px20 divider_style: solid21intersections:22 style: none23 color: transparent24 size: 0px25design:26 colors:27 ink: "#0e0e0e"28 surface: "#faf8f3"29 accent: "#0e0e0e"30 muted: "#8a8680"31 hairline: "#e0dcd2"32 fonts:33 display: Fraunces34 body: "Source Serif 4"35 mono: "JetBrains Mono"36 radius: 0px37 google_fonts_url: "https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,500;0,9..144,700;1,9..144,400&family=Source+Serif+4:opsz,wght@8..60,400;8..60,500&family=JetBrains+Mono:wght@400;500;600&display=swap"38---39 40# Sticky Index Rail41 42## AI Build Instructions43 44> **Read this section before writing any code.** The rules below45> are non-negotiable. Every value used in the UI must come from this46> file's frontmatter — never substitute, approximate, or invent new47> colors, fonts, radii, or shadows. If a value is missing, ask the48> user before adding one.49 50### 1 · Your role51 52You are building UI for a project that has adopted **Sticky Index Rail** as its53design system. Treat `PATTERN.md` as the single source of truth.54Your job is to translate the user's product requirements into55components and pages that look like they were designed by the same56person who authored this file.57 58### 2 · Token compliance59 60- Pull every color, font family, radius, shadow, and spacing value61 from the frontmatter at the top of this file.62- Use semantic roles (e.g. `primary`, `accent`, `muted`) — never63 hard-code hex values that bypass the system.64- When a token can be expressed as a CSS variable, declare it once65 in your global stylesheet and reference it everywhere downstream.66- The Google Fonts `<link>` is provided in the Typography section.67 Add it to `<head>` before any component renders.68 69### 3 · Build recipes70 71#### Page skeleton (the layout contract)72 73- Container: `centered`74- Content max-width: `1080px` (typography respects this even when the page is full-bleed).75- Vertical grid: **2 column hairlines** (capped at 2 on wide viewports), drawn with `0px solid transparent`.76- Section padding: `120px` top + bottom inside every section.77- Section divider: `1px solid rgba(15, 15, 15, 0.06)` between sections.78 79#### Primary CTA80 81Exactly **one** primary CTA per page or section. The pattern's discipline depends on this.82 83- Background: `#0e0e0e` · Color: `#faf8f3`84- Padding: `11px 22px` · Weight: `600`85- Shape: `sharp` (radius: `0px`)86 87#### Headlines88 89- Family: `Fraunces` · Size: `clamp(2.25rem, 4vw, 3rem)` · Leading: `1.05` · Weight: `700`90- Tracking: `-0.02em`91- The pattern's signature: split the headline so the **second clause is italic in the accent color**. Example: "A clearer way to *say less.*"92 93#### Body copy94 95- Family: `Source Serif 4` · Size: `1rem` · Leading: `1.65` · Color: `#8a8680`96- Max line length: 60–66 characters. Never let prose stretch the full content width.97 98#### Eyebrows / metadata99 100- Family: `JetBrains Mono` · Size: `0.6875rem` · Letter-spacing: `0.18em`101- Uppercased. Color: `#0e0e0e`.102 103### 4 · Hard constraints104 105Never do any of the following without explicit instruction from the user:106 107- Introduce a new color, font, radius, or shadow that isn't declared above.108- Mix this system with another (e.g. don't paste in Material or Bootstrap defaults).109- Use generic gradient defaults (purple→blue, peach→pink) — they break the system's voice.110- Reach for emoji icons. Use a consistent icon library and size icons in line with body type.111- Add motion that exceeds the system's restraint — keep transitions short (≤200ms) and subtle.112- Break the layout contract: the column count, divider rhythm, and content max-width are part of the pattern.113 114### 5 · Before you finish — verify115 116Run through this checklist for every screen you produce:117 118- [ ] Every color used appears in the Colors table above.119- [ ] Headlines use the display font; body copy uses the body font.120- [ ] Buttons match one of the declared variants exactly (shape, padding, weight).121- [ ] Border-radius values come from `radius.sm` / `radius.md` / `radius.lg` / `radius.pill`.122- [ ] Cards and dividers use the declared border + shadow tokens.123- [ ] The page respects the pattern's grid (column count + content max-width).124- [ ] Section dividers use the declared color, width, and style.125- [ ] Exactly one primary CTA per section — never duplicate.126- [ ] No values were invented; if you needed something missing, you stopped and asked.127 128---129 130## Overview131 132The Sticky Index Rail mirrors the table of contents you find inside scholarly133documents: a narrow rail on the left that lists every section by number and134title, and that highlights the active section as the user scrolls past it.135The right side of the page carries the actual content in a single comfortable136reading column.137 138The rail does three jobs at once. It tells the reader where they are in the139document, it tells them how much remains, and it gives them one click to jump140between sections without losing context. None of this requires JavaScript141beyond a single `IntersectionObserver` to update the active item.142 143## When to use it144 145- Long-form documentation, specs, RFCs, design system references.146- Multi-part landing pages where the user benefits from seeing the whole147 argument structure at a glance.148- Case studies and reports with 4–8 named sections.149- Privacy policies, terms, and other dense reference content.150 151## When to avoid it152 153- Pages with fewer than 3 sections — the rail looks empty.154- Pages with more than ~10 sections — the rail becomes a wall of text.155- Marketing pages that are meant to be read top-to-bottom in one pass.156- Mobile widths below 1024px. Hide the rail and replace it with a section157 number eyebrow inside each section.158 159## Do160 161- Number every section in mono caps (01, 02, 03). Numbers are what make the162 rail read as instrument, not navigation menu.163- Hold the rail at 220–260px wide. Narrower and titles wrap awkwardly; wider164 and the content column suffers.165- Use a single 2px ink bar to mark the active item. No background fill, no166 pill shape — just the bar.167- Keep the rail `position: sticky` with `top: 96px` so it sits below any168 page chrome but stays visible as the user scrolls.169 170## Don't171 172- Don't add icons next to the rail items. Numbers + titles only.173- Don't use a colored fill to mark the active item. The 2px bar is the device.174- Don't make the rail scroll-locked separately from the page. Sticky only.175- Don't omit the section numbers — the numbers are the entire signature.176 177## Notes178 179- The active-state mechanism is one `IntersectionObserver` watching every180 `<section data-index>` element. When a section crosses 30% of the viewport181 from the top, mark its rail item active.182- The rail color should be derived from the system foreground: inactive items183 at ~50% alpha, active item at full opacity. The bar inherits the same.184- Pair with Centered Column or Marginalia Notes for the content side.185 186---187 188## Tokens189 190> Generated from the same source the live preview renders from.191> Treat the values below as the contract — never substitute approximations.192 193### Container194 195| Property | Value |196|----------|-------|197| container | `centered` |198| contentMaxWidth | `1080px` |199| pagePadding | `80px` |200 201### Vertical Grid202 203| Property | Value |204|----------|-------|205| columns | `2` |206| maxColumns | `2` |207| lineColor | `transparent` |208| lineWidth | `0px` |209| lineStyle | `solid` |210| edgeLines | `false` |211 212### Section Dividers213 214| Property | Value |215|----------|-------|216| paddingY | `120px` |217| dividerColor | `rgba(15, 15, 15, 0.06)` |218| dividerWidth | `1px` |219| dividerStyle | `solid` |220 221### Intersections222 223| Property | Value |224|----------|-------|225| style | `none` |226| color | `transparent` |227| size | `0px` |228 229## Design Identity230 231> This pattern ships with its own typography, color, and CTA tokens.232> Use the values below verbatim — they are the system, not a starting point.233 234### Colors235 236| Token | Value |237|-------|-------|238| ink (primary text) | `#0e0e0e` |239| surface (page background) | `#faf8f3` |240| accent (single moment per page) | `#0e0e0e` |241| muted (metadata, captions) | `#8a8680` |242| hairline (rules and dividers) | `#e0dcd2` |243 244### Typography245 246Load via Google Fonts:247 248```html249<link rel="preconnect" href="https://fonts.googleapis.com">250<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>251<link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,500;0,9..144,700;1,9..144,400&family=Source+Serif+4:opsz,wght@8..60,400;8..60,500&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">252```253 254| Role | Family |255|------|--------|256| display (headlines) | `Fraunces` |257| body (prose) | `Source Serif 4` |258| mono (metadata, numerals) | `JetBrains Mono` |259 260### Type Scale261 262| Role | Size | Leading | Weight | Tracking |263|------|------|---------|--------|----------|264| Hero / H1 | `clamp(2.25rem, 4vw, 3rem)` | `1.05` | `700` | `-0.02em` |265| Body | `1rem` | `1.65` | `400` | — |266| Eyebrow | `0.6875rem` | — | `600` | `0.18em` |267 268> The hero pairs roman + italic — split the headline so the secondary clause renders italic in the accent color.269 270### Primary CTA271 272| Property | Value |273|----------|-------|274| shape | `sharp` |275| background | `#0e0e0e` |276| color | `#faf8f3` |277| padding | `11px 22px` |278| fontWeight | `600` |279| radius | `0px` |280 281> One CTA per page. The pattern's discipline depends on this — never duplicate.282 283---284 285## Reference Implementation286 287Copy-paste-ready HTML + CSS that renders this pattern with the exact token288values declared above. Theme the colors against your system's hairline tone.289 290### HTML291 292```html293<div class="layout">294 <!-- Sticky rail with numbered section index. -->295 <nav class="rail" aria-label="Document index">296 <ol>297 <li><a href="#s1" data-target="s1" class="is-active">298 <span class="num">01</span><span class="ttl">Overview</span></a></li>299 <li><a href="#s2" data-target="s2">300 <span class="num">02</span><span class="ttl">Architecture</span></a></li>301 <li><a href="#s3" data-target="s3">302 <span class="num">03</span><span class="ttl">Tokens</span></a></li>303 <li><a href="#s4" data-target="s4">304 <span class="num">04</span><span class="ttl">Examples</span></a></li>305 </ol>306 </nav>307 308 <main class="content">309 <section id="s1" data-index><h2>Overview</h2><p>...</p></section>310 <section id="s2" data-index><h2>Architecture</h2><p>...</p></section>311 <section id="s3" data-index><h2>Tokens</h2><p>...</p></section>312 <section id="s4" data-index><h2>Examples</h2><p>...</p></section>313 </main>314</div>315 316<script>317 const links = document.querySelectorAll(".rail a");318 const obs = new IntersectionObserver((entries) => {319 entries.forEach((e) => {320 if (e.isIntersecting) {321 links.forEach((l) => l.classList.remove("is-active"));322 document323 .querySelector(`.rail a[data-target="${e.target.id}"]`)324 ?.classList.add("is-active");325 }326 });327 }, { rootMargin: "-30% 0px -60% 0px" });328 document.querySelectorAll("[data-index]").forEach((el) => obs.observe(el));329</script>330```331 332### CSS333 334```css335:root {336 --max: 1080px;337 --rail-w: 240px;338 --gap: 64px;339 --section-y: 120px;340 --divider: rgba(15, 15, 15, 0.06);341 --fg-mute: rgba(15, 15, 15, 0.50);342 --fg: rgba(15, 15, 15, 1);343}344 345.layout {346 max-width: var(--max);347 margin: 0 auto;348 padding: 0 32px;349 display: grid;350 grid-template-columns: var(--rail-w) 1fr;351 column-gap: var(--gap);352}353 354/* Sticky rail — sits below any header chrome at top: 96px. */355.rail {356 position: sticky;357 top: 96px;358 align-self: start;359 height: max-content;360 padding: 8px 0;361}362 363.rail ol { list-style: none; padding: 0; margin: 0; }364 365.rail a {366 display: grid;367 grid-template-columns: 32px 1fr;368 gap: 12px;369 padding: 8px 0 8px 12px;370 text-decoration: none;371 color: var(--fg-mute);372 font-size: 0.875rem;373 border-left: 2px solid transparent;374 transition: color 120ms, border-color 120ms;375}376 377.rail .num {378 font-family: ui-monospace, "JetBrains Mono", monospace;379 font-size: 0.6875rem;380 letter-spacing: 0.10em;381}382.rail .ttl { letter-spacing: -0.005em; }383 384.rail a:hover { color: var(--fg); }385 386/* Active state — single 2px ink bar, full-opacity label. No fill. */387.rail a.is-active {388 color: var(--fg);389 border-left-color: var(--fg);390}391 392.content section {393 padding: var(--section-y) 0;394 border-bottom: 1px solid var(--divider);395}396.content h2 { font-size: clamp(1.75rem, 3vw, 2.5rem); line-height: 1.15; }397.content p { line-height: 1.65; max-width: 65ch; }398 399/* Mobile: hide the rail entirely; rely on inline section numbers. */400@media (max-width: 1024px) {401 .layout { grid-template-columns: 1fr; }402 .rail { display: none; }403 .content section { padding: 64px 0; }404}405```406 <div class="layout"> <!-- Sticky rail with numbered section index. --> <nav class="rail" aria-label="Document index"> <ol> <li><a href="#s1" data-target="s1" class="is-active"> <span class="num">01</span><span class="ttl">Overview</span></a></li> <li><a href="#s2" data-target="s2"> <span class="num">02</span><span class="ttl">Architecture</span></a></li> <li><a href="#s3" data-target="s3"> <span class="num">03</span><span class="ttl">Tokens</span></a></li> <li><a href="#s4" data-target="s4"> <span class="num">04</span><span class="ttl">Examples</span></a></li> </ol> </nav> <main class="content"> <section id="s1" data-index><h2>Overview</h2><p>...</p></section> <section id="s2" data-index><h2>Architecture</h2><p>...</p></section> <section id="s3" data-index><h2>Tokens</h2><p>...</p></section> <section id="s4" data-index><h2>Examples</h2><p>...</p></section> </main></div> <script> const links = document.querySelectorAll(".rail a"); const obs = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting) { links.forEach((l) => l.classList.remove("is-active")); document .querySelector(`.rail a[data-target="${e.target.id}"]`) ?.classList.add("is-active"); } }); }, { rootMargin: "-30% 0px -60% 0px" }); document.querySelectorAll("[data-index]").forEach((el) => obs.observe(el));</script>A 1/3 + 2/3 vertical split with a single hairline running the full page height. The narrow column carries metadata and eyebrow type; the wide column carries the lead. Classic magazine architecture for landing pages.
The structure does the talking. Typography, spacing, and one accent — nothing else competing for attention.
Full-bleed horizontal sections stacked floor-to-ceiling, alternating between plain surface and a diagonal-stripe band. No vertical grid — the rhythm comes from the section heights and the hard hairline between them.
The structure does the talking. Typography, spacing, and one accent — nothing else competing for attention.