Back to directory
Pattern · layout · navigation · sticky · long-read

Sticky Index Rail.

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.

01 · Preview

The kitchen sink.

Sticky.app
ProductPricingDocsAbout
Sign inGet started
layout · navigation · sticky

Build with Sticky Index Rail.

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.

Start building →View on GitHub
Featured
Tokens that travel with your prompts.
Active users
12,840
Status
All systems operational
01 · Color

Palette

primary
#0E0E0E
secondary
#8A8680
tertiary
#0E0E0E
neutral
#FAF8F3
surface
#FAF8F3
02 · Typography

Type scale

display
The quick brown fox
h1
A major section heading
h2
A subsection title
body
Body copy in the system's body font.
03 · Buttons

Buttons

PrimarySecondaryOutlineGhostText link →
Disabled
04 · Inputs

Form controls

Email
iris@studio.com
Password
••••••••••••
Bio
Designing for long-form publications. Based in Kyoto.
05 · Choices

Select & toggle

Plan
Starter
Free forever
Pro
$18 / month
Team
$48 / month
Toggles
Public profile
Require two-factor
Auto-accept invites
06 · Tags & badges

Labeling

NeutralAccentSolidWith dot
NEWBETAv2.0LIVE
07 · Cards

Cards

Feature
Editorial rigor

Prose-first token file — decisions live next to their reasoning.

Learn more →
Metric
24,810
▲ +12.4% vs last week
08 · Navigation

Tabs & breadcrumb

Overview
Analytics
Members
Workspace/Projects/Heritage System
09 · Spacing

Spacing scale

Fine micro-scale (1–5px) for pills, editorial scale (12–21px) for the grid.

1px
2px
5px
8px · base
10px
12px
14px
16px
18px
20px
21px
10 · Radius

Border radius scale

The system's own radius tokens — sm for chips and inputs, md for buttons, lg for cards, pill for fully-rounded CTAs.

sm
0px
md
0px
lg
0px
pill
999px
11 · Elevation

Depth & elevation

00 · Flat
Elevation 00
01 · Low
Elevation 01
02 · Medium
Elevation 02
03 · High
Elevation 03
12 · Data

Charts

Weekly revenue
$48,210
M
T
W
T
F
S
S
Active sessions
2,184
02 · Layout

The structural backbone.

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.

Sticky.
WorkJournalIndex
layout · navigation

A clearer way to say less.

The structure does the talking. Typography, spacing, and one accent — nothing else competing for attention.

6 min · April 2026
01 — Foundation
02 — Rhythm
03 — Restraint
03 · The file

PATTERN.md

markdown
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: pattern
6container: centered
7content_max_width: 1080px
8page_padding: 80px
9grid:
10 columns: 2
11 max_columns: 2
12 line_color: transparent
13 line_width: 0px
14 line_style: solid
15 edge_lines: false
16sections:
17 padding_y: 120px
18 divider_color: "rgba(15, 15, 15, 0.06)"
19 divider_width: 1px
20 divider_style: solid
21intersections:
22 style: none
23 color: transparent
24 size: 0px
25design:
26 colors:
27 ink: "#0e0e0e"
28 surface: "#faf8f3"
29 accent: "#0e0e0e"
30 muted: "#8a8680"
31 hairline: "#e0dcd2"
32 fonts:
33 display: Fraunces
34 body: "Source Serif 4"
35 mono: "JetBrains Mono"
36 radius: 0px
37 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 Rail
41
42## AI Build Instructions
43
44> **Read this section before writing any code.** The rules below
45> are non-negotiable. Every value used in the UI must come from this
46> file's frontmatter — never substitute, approximate, or invent new
47> colors, fonts, radii, or shadows. If a value is missing, ask the
48> user before adding one.
49
50### 1 · Your role
51
52You are building UI for a project that has adopted **Sticky Index Rail** as its
53design system. Treat `PATTERN.md` as the single source of truth.
54Your job is to translate the user's product requirements into
55components and pages that look like they were designed by the same
56person who authored this file.
57
58### 2 · Token compliance
59
60- Pull every color, font family, radius, shadow, and spacing value
61 from the frontmatter at the top of this file.
62- Use semantic roles (e.g. `primary`, `accent`, `muted`) — never
63 hard-code hex values that bypass the system.
64- When a token can be expressed as a CSS variable, declare it once
65 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 recipes
70
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 CTA
80
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#### Headlines
88
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 copy
94
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 / metadata
99
100- Family: `JetBrains Mono` · Size: `0.6875rem` · Letter-spacing: `0.18em`
101- Uppercased. Color: `#0e0e0e`.
102
103### 4 · Hard constraints
104
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 — verify
115
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## Overview
131
132The Sticky Index Rail mirrors the table of contents you find inside scholarly
133documents: a narrow rail on the left that lists every section by number and
134title, 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 comfortable
136reading column.
137
138The rail does three jobs at once. It tells the reader where they are in the
139document, it tells them how much remains, and it gives them one click to jump
140between sections without losing context. None of this requires JavaScript
141beyond a single `IntersectionObserver` to update the active item.
142
143## When to use it
144
145- Long-form documentation, specs, RFCs, design system references.
146- Multi-part landing pages where the user benefits from seeing the whole
147 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 it
152
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 section
157 number eyebrow inside each section.
158
159## Do
160
161- Number every section in mono caps (01, 02, 03). Numbers are what make the
162 rail read as instrument, not navigation menu.
163- Hold the rail at 220–260px wide. Narrower and titles wrap awkwardly; wider
164 and the content column suffers.
165- Use a single 2px ink bar to mark the active item. No background fill, no
166 pill shape — just the bar.
167- Keep the rail `position: sticky` with `top: 96px` so it sits below any
168 page chrome but stays visible as the user scrolls.
169
170## Don't
171
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## Notes
178
179- The active-state mechanism is one `IntersectionObserver` watching every
180 `<section data-index>` element. When a section crosses 30% of the viewport
181 from the top, mark its rail item active.
182- The rail color should be derived from the system foreground: inactive items
183 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## Tokens
189
190> Generated from the same source the live preview renders from.
191> Treat the values below as the contract — never substitute approximations.
192
193### Container
194
195| Property | Value |
196|----------|-------|
197| container | `centered` |
198| contentMaxWidth | `1080px` |
199| pagePadding | `80px` |
200
201### Vertical Grid
202
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 Dividers
213
214| Property | Value |
215|----------|-------|
216| paddingY | `120px` |
217| dividerColor | `rgba(15, 15, 15, 0.06)` |
218| dividerWidth | `1px` |
219| dividerStyle | `solid` |
220
221### Intersections
222
223| Property | Value |
224|----------|-------|
225| style | `none` |
226| color | `transparent` |
227| size | `0px` |
228
229## Design Identity
230
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### Colors
235
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### Typography
245
246Load via Google Fonts:
247
248```html
249<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 Scale
261
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 CTA
271
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 Implementation
286
287Copy-paste-ready HTML + CSS that renders this pattern with the exact token
288values declared above. Theme the colors against your system's hairline tone.
289
290### HTML
291
292```html
293<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 document
323 .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### CSS
333
334```css
335: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
04 · Reference implementation

Copy-paste ready.

html
<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>
05 · Keep browsing

Try another pattern.