Chapter 12
Capstone: a mini design system documentation site
A capstone brief for building a documentation site for a fictional design system, with required features, a suggested architecture that reuses earlier chapters' code, milestones, deliverables, and a self-review rubric.
When someone on a product team needs a tabs component, they open the docs, search for “tabs”, read when to use it, try the demo with a keyboard, copy the markup, and check the accessibility notes. If the search is slow, the demo is broken, or the examples don’t match the real component, they copy an old pattern from another page instead, and the inconsistency spreads.
In this capstone you build that documentation site for a small fictional design system. It pulls everything in the book together, and it is the kind of interface design engineers and design system teams build and maintain for real. It has navigation, search, filters, interactive demos, form validation, async loading, URL state, and saved preferences, and every one of those has to work with a keyboard and a screen reader.
The brief
Invent a design system with a name and a short purpose, for example an internal system for an employee services product. Document at least six components. Five of them should be the components you built in chapter 11: accordion or disclosure, tabs, dialog, alert, and form field. Add at least one more of your own choosing, such as a button, card, or empty state.
Each component gets a documentation section with a description, when to use it and when not to, an interactive demo, a code example, and accessibility notes. The site also needs the features below.
Required features
| Feature | What it means here | Chapters to draw on |
|---|---|---|
| Component navigation | A sidebar listing every component, with aria-current on the one being viewed | 1, 5, 9 |
| Searchable documentation | A search field that filters components by name and description as you type, with an empty state | 2, 5, 7 (debouncing) |
| Filterable examples by category | Filter components by category, such as actions, feedback, forms, and navigation | 3, 5 |
| Interactive demos | Working accordion, tabs, dialog, alert, and form field demos | 9, 11 |
| Keyboard behavior | Every control and demo works without a mouse, following each component’s contract | 9 |
| URL-based state | The search query, category, or selected component is in the URL, so a link restores the view | 8 |
| Saved theme preference | A light, dark, or system theme choice stored in localStorage | 8 |
| Active section tracking | IntersectionObserver highlights the section currently on screen in the navigation | 8 |
| A responsive demo | ResizeObserver drives at least one demo that adapts to its container’s width | 8 |
| A custom element | At least one custom element, optionally with Shadow DOM | 8, 11 |
| Accessible form validation | A form, such as “request a component” or “report a docs issue”, with the validation timing rule and accessible errors | 6, 11 |
| Async behavior | Component metadata loaded with fetch from a JSON file, with loading, error, and retry states | 7 |
| A debugging report | A write-up of at least two real bugs you hit, using the chapter 10 template | 10 |
Suggested architecture
Keep the same conventions as the rest of the book: plain HTML, CSS, and JavaScript modules, no build step, data attributes as hooks, and state in one place.
design-system-docs/
index.html
styles.css
app.js loads data, holds page state, wires everything together
data/
components.json component metadata: id, name, category, description
lib/
fake-api.js optional delay and failure helper from chapter 7
url-state.js read and write search params
storage.js localStorage helpers from chapter 8
theme.js theme preference from chapter 8
section-observer.js IntersectionObserver for active section tracking
render/
nav.js renders the component navigation
results.js renders search and filter results, including the empty state
components/ your chapter 11 library, reused as is
disclosure.js
tabs.js
alert.js
dialog.js
form-field.js
ui-swatch.js a custom element, for example a color token swatch
NOTES.md debugging report and technical explanation
Page state belongs in one object in app.js. The components keep their own internal state, as chapter 11 described, and report changes through events.
// app.js (excerpt)
const state = {
components: [],
status: 'loading', // 'loading' | 'success' | 'error'
query: '',
category: 'all',
};
Load the metadata the way chapter 7 did, checking response.ok, because fetch only rejects on network failure:
// app.js (excerpt)
async function loadComponents() {
state.status = 'loading';
render();
try {
const response = await fetch('data/components.json');
if (!response.ok) throw new Error(`Request failed with status ${response.status}`);
state.components = await response.json();
state.status = 'success';
} catch (error) {
console.error(error);
state.status = 'error';
}
render();
}
Keep the URL in step with state using replaceState, so typing in the search field doesn’t fill the back button’s history with one entry per keystroke:
// lib/url-state.js
const allowedCategories = ['all', 'actions', 'feedback', 'forms', 'navigation'];
export function readUrlState() {
const params = new URLSearchParams(location.search);
const category = params.get('category');
return {
query: params.get('q') ?? '',
category: allowedCategories.includes(category) ? category : 'all',
};
}
export function writeUrlState({ query, category }) {
const params = new URLSearchParams();
if (query) params.set('q', query);
if (category !== 'all') params.set('category', category);
const search = params.toString();
history.replaceState(null, '', search ? `?${search}` : location.pathname);
}
The URL check works the same way as chapter 8’s readLevelFromUrl: anyone can type anything into a URL, so an unknown category falls back to 'all'. Keep allowedCategories in step with the categories in components.json.
Reuse storage.js and theme.js from chapter 8, which already wrap localStorage in try/catch and check the saved value against the allowed themes. readSetting and writeSetting handle the JSON conversion and a blocked or full storage, and theme.js sets data-theme on <html>, with the CSS media query handling the “system” choice.
Milestones
Work in this order. Each milestone ends with something you can use and test, so a later problem never hides an earlier one.
- Static structure. Write the full HTML for the page and every component section with no JavaScript. The content should be readable and the navigation links should jump to each section. This is your progressive enhancement baseline.
- Data and rendering. Load
components.json, render the navigation and results from state, and show the loading, error, and empty states. Add a retry button for the error state. - Search, filters, and URL state. Add search and category filters, debounce the search, and keep the URL in sync. Reload the page with a query in the URL and confirm the view is restored.
- Interactive demos. Drop in your chapter 11 components. Log their events from
app.jsto confirm the outputs work. - Browser APIs. Add the theme preference, active section tracking, the
ResizeObserverdemo, and the custom element. - The form. Build the “request a component” form with the form field module, an error summary on submit, double-submit prevention, and a fake async submit.
- Accessibility pass. Test the whole site with a keyboard, the devtools Accessibility pane, an automated checker such as axe, and a screen reader. Fix what you find and record it.
- Write-up. Write the debugging report and the technical explanation.
Deliverables
Ship the working site, served from a local folder or any static host. Alongside it, write a plain-English technical explanation in NOTES.md that covers:
- how state is managed, and which state belongs to the page versus each component;
- how events update the interface, including the custom events your components send;
- how rendering works, and when you rebuild a list versus update one element;
- how accessibility was handled, component by component;
- how async behavior was handled, including errors and stale responses;
- which browser APIs you used and why each one fit;
- what bugs you hit;
- how you debugged them, using the chapter 10 template;
- what you would improve next.
Write the explanation for a reviewer who hasn’t seen your code. If you can explain how each part works and why you chose it, you understand the project well enough to change it, defend it in a review, and build the next one without a tutorial.
Self-review rubric
Score each area before you call the project done. “Solid” is the target. “Strong” is what you’d want before showing the work in a portfolio.
| Area | Needs work | Solid | Strong |
|---|---|---|---|
| State | State is spread across variables and the DOM | Page state lives in one object, and counts and filtered lists are derived | State boundaries between the page and components are clear and documented |
| Rendering | Lists flicker or lose focus, or text is inserted with innerHTML | Rendering is driven by state, uses textContent, and has empty and error states | Focus is preserved across renders, and updates are as small as they need to be |
| Events | Listeners are attached inside render functions or duplicated | Delegation is used for lists, and components emit namespaced events | The page reacts to component events without reaching into component internals |
| Accessibility | Some demos fail with a keyboard | Every control works with a keyboard and has a correct name, role, and state | Screen reader testing is done and recorded, and live regions announce status changes |
| Async | A failed request leaves a spinner forever | Loading, error, retry, and empty states all work | Slow and failing networks have been tested with throttling and the fake failure helper |
| Browser APIs | Features work only on first load | URL state, theme, and observers all work after a reload | Each API choice is explained, including what happens when storage is unavailable |
| Code organization | One large file | Modules with clear names, each with one job | Components are documented with options, events, and known limitations |
| Explanation | Describes what the code is | Explains how each part works | Explains why each choice was made and what you’d change next |
Where to go from here
This book stopped deliberately before TypeScript, frameworks, build tools, and testing infrastructure, so that you could learn what those tools are built on. You now know what a component is, how state and events flow through one, and what its accessibility contract requires, which is most of what makes those tools understandable.
The next step is the series’ other book, Building your own component system. It takes the same ideas into a production setting: TypeScript, React components built on an accessibility library, design tokens, theming, testing accessible components, Storybook as a documentation layer, and packaging a library for other teams. Your capstone site is a useful reference while you read it, because you’ll recognize your own init functions, events, and documentation pages in the more structured versions it builds.
Found an error? Report it.