Skip to main content
UX Design Engineering
Theme
Text size
100%
Typeface

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

FeatureWhat it means hereChapters to draw on
Component navigationA sidebar listing every component, with aria-current on the one being viewed1, 5, 9
Searchable documentationA search field that filters components by name and description as you type, with an empty state2, 5, 7 (debouncing)
Filterable examples by categoryFilter components by category, such as actions, feedback, forms, and navigation3, 5
Interactive demosWorking accordion, tabs, dialog, alert, and form field demos9, 11
Keyboard behaviorEvery control and demo works without a mouse, following each component’s contract9
URL-based stateThe search query, category, or selected component is in the URL, so a link restores the view8
Saved theme preferenceA light, dark, or system theme choice stored in localStorage8
Active section trackingIntersectionObserver highlights the section currently on screen in the navigation8
A responsive demoResizeObserver drives at least one demo that adapts to its container’s width8
A custom elementAt least one custom element, optionally with Shadow DOM8, 11
Accessible form validationA form, such as “request a component” or “report a docs issue”, with the validation timing rule and accessible errors6, 11
Async behaviorComponent metadata loaded with fetch from a JSON file, with loading, error, and retry states7
A debugging reportA write-up of at least two real bugs you hit, using the chapter 10 template10

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.

  1. 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.
  2. 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.
  3. 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.
  4. Interactive demos. Drop in your chapter 11 components. Log their events from app.js to confirm the outputs work.
  5. Browser APIs. Add the theme preference, active section tracking, the ResizeObserver demo, and the custom element.
  6. 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.
  7. 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.
  8. 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.

AreaNeeds workSolidStrong
StateState is spread across variables and the DOMPage state lives in one object, and counts and filtered lists are derivedState boundaries between the page and components are clear and documented
RenderingLists flicker or lose focus, or text is inserted with innerHTMLRendering is driven by state, uses textContent, and has empty and error statesFocus is preserved across renders, and updates are as small as they need to be
EventsListeners are attached inside render functions or duplicatedDelegation is used for lists, and components emit namespaced eventsThe page reacts to component events without reaching into component internals
AccessibilitySome demos fail with a keyboardEvery control works with a keyboard and has a correct name, role, and stateScreen reader testing is done and recorded, and live regions announce status changes
AsyncA failed request leaves a spinner foreverLoading, error, retry, and empty states all workSlow and failing networks have been tested with throttling and the fake failure helper
Browser APIsFeatures work only on first loadURL state, theme, and observers all work after a reloadEach API choice is explained, including what happens when storage is unavailable
Code organizationOne large fileModules with clear names, each with one jobComponents are documented with options, events, and known limitations
ExplanationDescribes what the code isExplains how each part worksExplains 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.