Project: A Documentation Site
Sidebar navigation, an on this page menu, code blocks that scroll, a search box and deep links that survive a sticky header. The hardest layout to get right.
- The brief
- The layout
- The decisions
- Two skip links
- Every nav is labelled
- Main comes before the table of contents in the source
- Fragment links clear the sticky header
- Code blocks scroll inside themselves
- A copy button on code blocks
- Highlighting the current section
- Escaping code samples
- Search
- Checking your work
- Extensions
- Practice
-
HTML Basics
- What is HTML: The Structure Layer of Every Web Page
- HTML Document Structure: DOCTYPE, html, head and body
- Elements, Tags and Attributes: The Vocabulary of HTML
- HTML Comments: Notes That Ship With Your Code
- Block Level and Inline Elements
- Writing and Running Your First HTML Page
- How a Browser Turns Markup Into a Page
- Text and Formatting
- Links and Navigation
- Images and Media
- Lists
- Tables
-
Forms
- Form Structure: form, action and method
- Input Types: Text, Email, Number, Date and the Rest
- Labels: The Most Important Element in a Form
- Checkboxes, Radio Buttons and Grouping
- select, option, optgroup and datalist
- textarea, File Uploads and Hidden Fields
- Buttons: submit, reset and button
- Built In Form Validation
- GET or POST: What Happens When a Form Is Submitted
- Semantic HTML
- HTML5 Features
- Head and Metadata
- HTML with CSS
- HTML with JavaScript
- Accessibility
-
HTML SEO
- How Google Works: Crawling, Indexing and Ranking
- SEO Friendly HTML Structure
- Titles and Descriptions That Earn Clicks
- Headings and Content Structure for Search
- Internal Linking and Anchor Text
- robots.txt and XML Sitemaps
- Canonical URLs and Duplicate Content
- Structured Data and JSON-LD
- Image SEO
- Core Web Vitals and Mobile Friendliness
- DevTools and Debugging
- Editor Productivity
- HTML Best Practices
- HTML Projects
- Advanced Projects
- Practice and Exams
The brief
Build a documentation site: a persistent sidebar of sections, a page level table of contents, code samples, and deep links that work.
It is the layout with the most competing demands - three navigation regions, long content, and a reading order that must still make sense on a phone.
The layout
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<a href="#doc-nav" class="skip-link">Skip to documentation menu</a>
<header class="docs-header">
<a href="/" class="logo"><img src="/logo.svg" alt="Riverside Docs home" width="140" height="32"></a>
<search>
<form action="/search" method="get" role="search">
<label for="docs-search" class="visually-hidden">Search the documentation</label>
<input type="search" id="docs-search" name="q" placeholder="Search">
<button type="submit">Search</button>
</form>
</search>
<button type="button" class="nav-toggle" aria-expanded="false" aria-controls="doc-nav">
Menu
</button>
</header>
<div class="docs-layout">
<nav id="doc-nav" aria-label="Documentation" class="sidebar">
<h2 class="visually-hidden">Documentation sections</h2>
<h3>Getting started</h3>
<ul>
<li><a href="/docs/install">Installation</a></li>
<li><a href="/docs/quick-start">Quick start</a></li>
</ul>
<h3>Guides</h3>
<ul>
<li><a href="/docs/forms" aria-current="page">Forms</a></li>
<li><a href="/docs/tables">Tables</a></li>
</ul>
</nav>
<main id="main-content" tabindex="-1">
<nav aria-label="Breadcrumb">
<ol>
<li><a href="/docs">Docs</a></li>
<li><a href="/docs/guides">Guides</a></li>
<li><a href="/docs/forms" aria-current="page">Forms</a></li>
</ol>
</nav>
<article>
<h1>Forms</h1>
<p class="lead">How to build a form that works for everyone.</p>
<h2 id="labels">Labels</h2>
<p>...</p>
<pre><code class="language-html"><label for="email">Email</label>
<input type="email" id="email" name="email"></code></pre>
<h2 id="validation">Validation</h2>
<p>...</p>
<footer class="page-nav">
<a href="/docs/quick-start" rel="prev">Previous: Quick start</a>
<a href="/docs/tables" rel="next">Next: Tables</a>
</footer>
</article>
</main>
<nav aria-labelledby="toc-heading" class="toc">
<h2 id="toc-heading">On this page</h2>
<ol>
<li><a href="#labels">Labels</a></li>
<li><a href="#validation">Validation</a></li>
</ol>
</nav>
</div>
</body>The decisions
Two skip links
With three navigation regions, one skip link is not enough. Offering both skip to content and skip to the documentation menu covers the two things a returning reader actually wants.
Every nav is labelled
Four navigation landmarks on one page - main, documentation, breadcrumb and table of contents - and without labels a screen reader announces four identical navigation regions.
Main comes before the table of contents in the source
On a phone the layout collapses to a single column in source order. Putting the table of contents after the content means a reader is not made to scroll past a list of links to reach the article. CSS grid puts it back on the right for wide screens.
.docs-layout {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
max-width: 90rem;
margin-inline: auto;
padding-inline: 1rem;
}
@media (min-width: 64rem) {
.docs-layout {
grid-template-columns: 16rem minmax(0, 1fr) 14rem;
grid-template-areas: "sidebar main toc";
}
.sidebar { grid-area: sidebar; }
main { grid-area: main; }
.toc { grid-area: toc; }
}
@media (min-width: 64rem) {
.sidebar, .toc {
position: sticky;
top: 5rem;
align-self: start;
max-height: calc(100vh - 6rem);
overflow-y: auto;
}
}Note minmax(0, 1fr) on the main column. Without it, a long unbroken line in a code block forces the whole grid wider than the viewport.
Fragment links clear the sticky header
:is(h1, h2, h3, h4)[id] {
scroll-margin-top: 5rem;
}Without this, following a table of contents link lands the heading underneath the fixed header and the reader sees the wrong section.
Code blocks scroll inside themselves
pre {
overflow-x: auto;
padding: 1rem;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
tab-size: 2;
}
pre code { font-family: ui-monospace, "Cascadia Mono", Consolas, monospace; }A wide code block with no scroll container makes the entire page scroll sideways on a phone, which is one of the most common documentation site failures.
A copy button on code blocks
document.querySelectorAll("pre > code").forEach((code) => {
const pre = code.parentElement;
const button = document.createElement("button");
button.type = "button";
button.className = "copy";
button.textContent = "Copy";
button.setAttribute("aria-label", "Copy code to clipboard");
button.addEventListener("click", async () => {
await navigator.clipboard.writeText(code.textContent);
button.textContent = "Copied";
setTimeout(() => { button.textContent = "Copy"; }, 1800);
});
pre.append(button);
});The button announces its state change through its own text, which a screen reader reports when it is activated.
Highlighting the current section
const headings = document.querySelectorAll("main h2[id]");
const links = document.querySelectorAll(".toc a");
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
const id = entry.target.id;
links.forEach((link) => {
link.setAttribute("aria-current",
link.getAttribute("href") === `#${id}` ? "true" : "false");
});
});
}, { rootMargin: "-20% 0px -70% 0px" });
headings.forEach((h) => observer.observe(h));Escaping code samples
Documentation shows markup, so every sample must be entity escaped:
<!-- this renders as a real input -->
<pre><code><input type="email"></code></pre>
<!-- this shows the markup -->
<pre><code><input type="email"></code></pre>Escape the ampersand first when doing it by hand, or everything after it double escapes.
Search
A documentation search does not need a server. A generated JSON index and a client side search is enough for a few hundred pages:
<search>
<form action="/search" method="get" role="search">
<label for="q" class="visually-hidden">Search the documentation</label>
<input type="search" id="q" name="q"
autocomplete="off"
aria-describedby="search-hint">
<small id="search-hint">Press / to focus</small>
<button type="submit">Search</button>
</form>
</search>
<div id="search-results" aria-live="polite"></div>Keeping the real form with a real action means search still works if the script fails.
Checking your work
- Validate every page.
- Tab through: two skip links, then header, then sidebar, then content.
- Follow every table of contents link and confirm the heading is not under the header.
- Check a wide code block at 320 pixels; the block should scroll, not the page.
- Confirm all four navigation landmarks are labelled distinctly.
- Test the copy buttons with a keyboard.
Extensions
- Add a version switcher with correct canonical tags per version.
- Add syntax highlighting, keeping the text selectable.
- Add a dark mode with a stored preference applied before first paint.
- Add
TechArticlestructured data. - Add a keyboard shortcut to focus search, ignoring key presses inside fields.
Practice
- Build the three column layout and confirm the source order is content before table of contents.
- Add
scroll-margin-topand test every fragment link under a sticky header. - Add copy buttons and use them with the keyboard alone.
- Test the whole layout at 320 pixels and list what needed changing.