1990s Documentation Page — Sidebar navigation, content layout with headings and code blocks, table of contents, and responsive mobile breakpoints. Every open-source project, API, and framework needs documentation. Inspired by freeCodeCamp's Responsive Web Design certification and documentation sites like Astro Docs.

01

HTML Structure

Complete

Why learn this?

Documentation pages are the most read type of technical content on the web. React, Vue, Astro, Tailwind — every major project has a two-column docs layout with a sidebar navigation and content area. The semantic HTML structure behind these pages (nav, main, section, headings hierarchy) is the same pattern used by Stripe's API docs, MDN Web Docs, and every framework site. Getting the structure right first makes everything else — styling, responsive behavior, accessibility — fall into place.

Design decisions & tradeoffs

Flexbox sidebar layout vs CSS Grid. The page uses display: flex with min-height: 100vh on the wrapper and a fixed-width sidebar (width: 260px + flex-shrink: 0). This prevents the sidebar from collapsing when the content area is narrow. The alternative — CSS Grid with grid-template-columns: 260px 1fr — achieves the same layout with one line of CSS on the parent. The downside: Grid introduces a gap between columns that's part of the grid, not padding, which can interact unpredictably with child margins. Flexbox gives more predictable control over internal spacing.

Semantic nav vs div-based sidebar. Using <nav> for the sidebar communicates to screen readers that this is a navigation landmark (WCAG 4.1.2). Users can skip directly to the nav via landmarks in screen reader navigation menus. A <div>-based sidebar would require role="navigation" to get the same semantics. Using semantic HTML from the start means zero extra work for accessibility.

Table of contents placement. The TOC sits at the top of the content area, below the page title. This follows the pattern used by Astro Docs and Tailwind Docs — it gives readers a quick overview of what the page covers before they scroll. An alternative is a second sidebar on the right (like MDN or React Docs), which requires more complex CSS for the 3-column layout and doesn't work well on mobile without collapsing.

Code block styling. Code examples use a dark background (#1a1a2e) matching the sidebar color scheme, which creates visual consistency. Inline <code> uses a light background (#eef2f5) with red text (#e74c3c) — the standard GitHub-style inline code pattern. This dual treatment (dark block / light inline) is the most common docs convention because it clearly distinguishes code snippets from inline references.

Browser compatibility

  • Flexbox: Supported since 2015 (IE11 with -ms- prefixes). The flex-shrink: 0 property works in all flexbox-supporting browsers. IE11 needs -ms-flex-negative: 0.
  • min-height: 100vh: Works universally. On iOS Safari, 100vh includes the URL bar height — the layout will be taller than the visible viewport on initial load. Fix with 100dvh (iOS 15.4+).
  • overflow-x: auto on pre: Creates a horizontal scrollbar when code lines exceed the content width. Works in all browsers. On Firefox, the scrollbar appears only when scrolling; on Chrome/Edge, it's always visible.
  • CSS scroll behavior: html { scroll-behavior: smooth } for anchor links is supported since Chrome 61, Firefox 36, Safari 15.4. Safari was the late adopter.

Accessibility details

  • Landmark navigation: The <nav> element creates a navigation landmark. Screen reader users can jump directly to it. Without it, keyboard users tab through every sidebar link to reach the content.
  • Heading hierarchy: h1h2h3 creates a clear document outline. Skipping levels (h1 → h3) breaks the outline for screen reader navigation. Each h2 represents a distinct page section.
  • Link underlines: TOC links use text-decoration: underline on hover, which meets WCAG 1.4.1 (Use of Color) — links are distinguishable by more than color alone.
  • Code contrast: Inline code uses #e74c3c on #eef2f5 background — contrast ratio ~5.5:1, above WCAG AA for normal text (4.5:1). The dark code blocks use #e6edf3 on #1a1a2e — contrast ratio ~12:1, exceeding AAA.
  • Active nav link: The .active class uses both color (#00d4aa) and font-weight to indicate the current page. This satisfies WCAG 1.4.1 — users who can't distinguish green still see the bold text.

Common pitfalls

  • Flex sidebar collapsing: Without flex-shrink: 0, the sidebar shrinks below its intended width when the content area is narrow. The sidebar text wraps awkwardly or gets clipped. Always set flex-shrink: 0 on fixed-width flex children.
  • Code wrapping vs scrolling: white-space: pre-wrap wraps long code lines, making them harder to read (wrapped lines look like new statements). overflow-x: auto with white-space: pre preserves line structure. Use pre-wrap for tutorials, pre + scroll for reference docs.
  • Missing id on section headings: Anchor links (href="#quick-start") need matching id attributes on target elements. Headings without id are unreachable via URL fragments — critical for sharing specific sections.
  • Sidebar link target size: Small click targets (padding: 0.2rem) fail WCAG 2.5.8 (minimum 24×24px for target size). The sidebar uses padding: 0.4rem 0.6rem × font-size: 0.9rem (~14.4px) for a total hit area of ~26px — meets the recommendation.

Key concepts

  • display: flex + flex-shrink: 0 — Sidebar layout that doesn't collapse. Grid alternative: grid-template-columns: 260px 1fr
  • <nav> — Semantic navigation landmark. Screen-reader accessible without extra ARIA
  • h1h2h3 — Document outline for accessibility and SEO
  • id on headings — Anchor targets for URL fragments and table of contents
  • overflow-x: auto on <pre> — Horizontal scroll for long code lines instead of wrapping
  • Active nav state — Uses both color and weight to indicate current page (accessible by default)

Next up

Step 2 adds sidebar polish: sticky positioning, scroll indicators, section grouping, and hover/active transitions.

Page layout ▶ Run
<div class="doc-layout">
  <nav class="sidebar">
    <h2>AgentForge Docs</h2>
    <ul>
      <li><a href="#">
        Getting Started</a></li>
      <li><a href="#">
        Installation</a></li>
      <li><a href="#">
        Configuration</a></li>
    </ul>
  </nav>

  <main class="content">
    <h1>Getting Started</h1>
    <p>Welcome to AgentForge.</p>

    <h2 id="quick-start">
      Quick Start</h2>
    <p>Install the CLI:</p>
    <pre><code>npm install -g @agentforge/cli</code></pre>

    <h2 id="core-concepts">
      Core Concepts</h2>
    <ul>
      <li>Agents</li>
      <li>Tools</li>
      <li>Workflows</li>
    </ul>
  </main>
</div>
Layout CSS
.doc-layout {
  display: flex;
  min-height: 100vh;
}
nav.sidebar {
  width: 260px;
  background: #1a1a2e;
  color: rgba(255,255,255,0.8);
  padding: 2rem 1.5rem;
  flex-shrink: 0;
}
main.content {
  flex: 1;
  max-width: 800px;
  padding: 3rem 2.5rem;
}
.content pre {
  background: #1a1a2e;
  color: #e6edf3;
  padding: 1.2em;
  border-radius: 8px;
  overflow-x: auto;
}
📐 Flexbox docs layout: flex-shrink: 0 is the most commonly forgotten property in sidebar layouts. Without it, the sidebar shrinks when the content doesn't fill the viewport. For complex docs with 3-column layouts (sidebar + content + right TOC), use CSS Grid instead: grid-template-columns: 260px 1fr 240px.
02

Sidebar Polish

Complete

Why learn this?

The sidebar is the primary navigation interface for documentation sites. A well-polished sidebar — sticky positioning, section grouping, visual active indicators, scroll tracking — is what separates a professional docs site from a basic one. Astro Docs, Tailwind Docs, and Stripe's API docs all use the same patterns: sticky sidebar that stays visible while scrolling, grouped navigation sections, and auto-highlighting the current section as you scroll.

Design decisions & tradeoffs

Sticky sidebar vs fixed sidebar. position: sticky; top: 0 keeps the sidebar in the viewport as the user scrolls the content. Unlike position: fixed, a sticky sidebar stays within its parent container's flow — if a top banner or announcement bar exists above the layout, the sidebar won't overlap it. The tradeoff: sticky requires the sidebar's parent to have a defined height. Using height: 100vh with overflow-y: auto ensures the sidebar fills the viewport and scrolls independently for long nav lists.

Section grouping. The sidebar groups related links under labeled sections ("Getting Started", "Core Concepts", "Guides", "Reference"). This follows the information architecture pattern used by Astro, Vue, and Tailwind docs — it helps users find what they need without scanning an alphabetical list. Each group has a small uppercase heading (font-size: 0.65rem; letter-spacing: 0.08em) that's present but unobtrusive. The alternative — a flat list — works for small docs but fails when navigation exceeds 10-15 items.

Left border active indicator. The active nav link gets a border-left: 3px solid #00d4aa combined with a slightly darker background (rgba(0,212,170,0.08)) and bolder text. This uses three distinct signals — position, color, and weight — to indicate the current page. The border is on the left because users read left-to-right; it acts as a visual anchor before the link text. The hover state also shows a dimmed version of the border (rgba(0,212,170,0.3)) to preview interactivity.

Scroll progress bar vs scroll-to-top. A thin gradient progress bar at the top of the page (position: fixed; height: 3px) shows how far the user has read. This is common in long-form documentation (MDN, Stripe). The alternative — a scroll-to-top button — is better for pages where users jump between sections frequently rather than reading linearly. The progress bar is implemented with a simple scroll listener updating style.width, which is performant because it only triggers repaints on the compositor layer.

Scroll-based active section tracking. JavaScript listens to the scroll event and compares each section's getBoundingClientRect().top against a threshold (150px). When a section enters the top region of the viewport, its corresponding sidebar link gains the .active class. The alternative — IntersectionObserver — is more performant (no scroll event listener) but requires more boilerplate. For a docs page with under 20 sections, the scroll listener approach is simpler and won't cause jank.

Browser compatibility

  • position: sticky: Supported since Chrome 56, Firefox 59, Safari 13. Fails silently if any parent has overflow: hidden — the #1 cause of "sticky not working" bugs.
  • overflow-y: auto on sidebar: Creates a scrollable sidebar when nav content exceeds viewport height. Works in all browsers. On macOS, scrollbars are hidden by default unless the user has "Show scroll bars: Always" enabled.
  • Custom scrollbar styling (::-webkit-scrollbar): WebKit-only (Chrome, Safari, Edge). Firefox uses scrollbar-width: thin. There's no universal custom scrollbar standard.
  • scroll-margin-top: Adds offset when jumping to a section via anchor link. Supported since Chrome 69, Firefox 68, Safari 14.1. Without it, the sticky sidebar can cover the section heading.
  • getBoundingClientRect() on scroll: The scroll listener fires at ~60fps. For performance, avoid layout-triggering reads (like offsetTop) in the same frame. getBoundingClientRect triggers a forced reflow — it's acceptable for a single call per frame but not for loops.

Accessibility details

  • Skip-link still needed: Even with a sticky sidebar, keyboard users tab through every sidebar link before reaching content. The skip-link from Step 4 of the Tribute Page should be added here too — it's non-negotiable for WCAG compliance.
  • Group headings are visual only: The <h3> elements in sidebar groups are styled as small uppercase labels. They're not navigation links — screen reader users navigating by heading might land on them expecting interactivity. Consider aria-hidden="true" or using <span> with role="heading" if they're purely decorative.
  • Active link uses multiple signals: The active state changes border, background, color, and font-weight. This means users with any single form of color vision deficiency can still identify the current page via the bold text or left border.
  • Scroll progress bar is decorative: The progress bar has no ARIA role — it's purely visual and doesn't need screen reader announcement. If it were interactive (clickable to jump), it would need role="progressbar" with aria-valuenow.
  • Custom scrollbar: Thin custom scrollbars (width: 6px) can fail WCAG 2.5.8 (Target Size) if the sidebar links are also small. Ensure link tap targets remain at least 24×24px.

Common pitfalls

  • Sticky sidebar + overflow-y clipping: height: 100vh on the sidebar with overflow-y: auto means the sidebar scrolls independently. If the sidebar is inside a display: flex parent without align-items: flex-start, the sidebar can stretch taller than 100vh and the sticky behavior breaks. Add align-items: flex-start to the flex parent.
  • Active link scroll tracking delay: The scroll event fires 60 times per second but the active class logic may not update until the user stops scrolling briefly. A requestAnimationFrame throttle or IntersectionObserver gives smoother transitions.
  • Multiple sections with the same threshold: When two sections are within the 150px threshold simultaneously (e.g., one ends and another begins), the last matching section wins. This can cause a "jump" where the active indicator skips a section as you scroll past. Fine-tune the threshold value per page layout.
  • Progress bar at top with sticky nav: If the page has a sticky top nav AND a scroll progress bar, they overlap. The progress bar needs z-index higher than the nav to be visible, or place it below the nav (at top: 50px).
  • scroll-margin-top mismatch: If the sidebar is 60px high (padding + content) but scroll-margin-top is only 20px, clicking an anchor link scrolls the heading behind the sticky sidebar. Match the margin to the sidebar height + gap.

Key concepts

  • position: sticky; top: 0 — Keeps sidebar visible during scroll. Fails silently if parent has overflow
  • height: 100vh; overflow-y: auto — Independent sidebar scrolling. Sidebar nav stays contained to viewport
  • border-left + background + font-weight — Triple-signal active state for accessibility
  • ::-webkit-scrollbar — Custom scrollbar styling. WebKit-only; Firefox uses scrollbar-width
  • scroll-margin-top — Offset for anchor-linked sections. Prevents sticky nav overlap
  • getBoundingClientRect().top — Scroll-based active section detection. IntersectionObserver is the modern alternative

Next up

Step 3 polishes the content area: typography scale, styled code blocks with syntax colors, better section anchors, and deeper table of contents.

Sticky sidebar ▶ Run
<nav class="sidebar">
  <h2>AgentForge Docs</h2>

  <div class="sidebar-group">
    <h3>Getting Started</h3>
    <ul>
      <li><a href="#quick-start"
        class="active">Quick Start</a></li>
      <li><a href="#installation">
        Installation</a></li>
    </ul>
  </div>

  <div class="sidebar-group">
    <h3>Core Concepts</h3>
    <ul>
      <li><a href="#agents">
        Agents</a></li>
      <li><a href="#tools">
        Tools</a></li>
    </ul>
  </div>
</nav>
Sidebar CSS
nav.sidebar {
  width: 260px;
  background: #1a1a2e;
  padding: 2rem 1.5rem;
  flex-shrink: 0;
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
}
.sidebar-group {
  margin-bottom: 1.5rem;
}
.sidebar-group h3 {
  font-size: 0.65rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: rgba(255,255,255,0.3);
  margin-bottom: 0.5rem;
  padding: 0 0.6rem;
}
.sidebar a {
  color: rgba(255,255,255,0.55);
  padding: 0.45rem 0.6rem;
  border-radius: 6px;
  transition: all 0.2s ease;
  border-left: 3px solid transparent;
}
.sidebar a:hover {
  color: #fff;
  border-left-color: rgba(0,212,170,0.3);
  padding-left: 0.9rem;
}
.sidebar a.active {
  color: #00d4aa;
  background: rgba(0,212,170,0.08);
  border-left-color: #00d4aa;
}
.sidebar::-webkit-scrollbar {
  width: 6px;
}
Scroll tracking JS
// Scroll progress bar
window.addEventListener('scroll', () => {
  const scrollTop =
    document.documentElement.scrollTop;
  const scrollHeight =
    document.documentElement.scrollHeight
    - window.innerHeight;
  const progress =
    (scrollTop / scrollHeight) * 100;
  document.getElementById('scrollProgress')
    .style.width = progress + '%';
});

// Active sidebar link on scroll
const sections =
  document.querySelectorAll('main h2[id]');
const navLinks =
  document.querySelectorAll('.sidebar a');
window.addEventListener('scroll', () => {
  let current = '';
  sections.forEach(section => {
    const top =
      section.getBoundingClientRect().top;
    if (top <= 150)
      current = section.getAttribute('id');
  });
  navLinks.forEach(link => {
    link.classList.remove('active');
    if (link.getAttribute('href')
      === '#' + current)
      link.classList.add('active');
  });
});
📍 Sticky gotcha: position: sticky stops working if any parent element has overflow: hidden, overflow: auto, or overflow: scroll. This is the most common sticky bug — the browser creates a new scroll container that clips the sticky behavior. Check every ancestor in the DOM tree.
03

Content & Code Blocks

Complete

Why learn this?

Content typography and code presentation are what separate a readable documentation site from an unreadable one. Users come to docs to read and copy code — if the typography is cramped or the code blocks are unstyled, they leave. Astro Docs, MDN, and Stripe's API docs all invest heavily in content typography: a clear type scale, distinct heading levels, readable line lengths, and syntax-highlighted code blocks with copy buttons. This step teaches you the typographic foundation that makes long-form technical content scannable and pleasant to read.

Design decisions & tradeoffs

Typography scale. The page uses a modular scale: h1: 2.25rem (36px), h2: 1.5rem (24px), h3: 1.15rem (~18px), body: 1rem (16px). Each level is approximately 1.25× the next — the standard "major third" scale used by Tailwind and Bootstrap. The h1 gets letter-spacing: -0.02em (tightened tracking) which is a common design technique for large headings — it makes them feel more deliberate and less "shouty." Smaller headings get progressively looser spacing. The line-height follows the same pattern: h1: 1.2, h2: 1.3, body: 1.7. Larger text needs tighter line-height; small text needs more space between lines for readability.

Lead paragraph. The intro paragraph uses font-size: 1.15rem with a muted color (#666) — slightly larger and lighter than body text. This is called a "lead" or "deck" and is standard in long-form writing (Medium, Smashing Magazine, documentation sites). It summarizes the page in 1-2 sentences before the user dives into sections. The alternative — jumping directly into headings — works for reference docs but makes tutorial-style pages feel abrupt.

Syntax-highlighted code blocks. Code blocks use CSS class-based syntax highlighting with semantic tokens: .kw (keywords — purple), .fn (functions — blue), .str (strings — green), .num (numbers — orange), .cm (comments — gray italic), .prop (properties — red). This follows the Material Theme color scheme, which has high contrast on dark backgrounds. The alternative — server-side syntax highlighting (Prism.js, Shiki) — is more comprehensive (supports 200+ languages) but requires a JS library or build step. CSS class highlighting is simpler for small docs but doesn't auto-detect languages.

Code block header with copy button. A thin header bar above each code block shows the filename or language ("Terminal", "JavaScript", "agentforge.yaml") and a "Copy" button. The filename helps users understand what type of code they're looking at — especially useful when a page has multiple languages. The copy button uses navigator.clipboard.writeText() which works in HTTPS contexts. The alternative — a single global copy button — is less intuitive because users have to find and select the block first.

Section anchor permalinks. Each heading gets an # anchor link that appears on hover (opacity: 0 → 1 transition). This is the standard pattern used by GitHub READMEs, MDN, and every major documentation site. The anchor link allows users to copy the URL to a specific section and share it. The position: absolute; left: -1.2em placement puts the # outside the heading text, so clicking the heading itself doesn't trigger navigation. scroll-margin-top: 2rem ensures the heading isn't hidden behind the sticky sidebar when navigating via anchor link.

Hierarchical table of contents. The TOC now shows nested sub-sections (toc-h2 for main sections, toc-h3 indented for sub-sections). The hierarchy mirrors the heading structure (h2h3) so users can see the page structure at a glance. The alternative — a flat list — is simpler to maintain but doesn't convey section relationships. MDN and Astro Docs both use hierarchical TOCs for pages with more than 5 sections.

Inline callout boxes. The .inline-demo class creates a highlighted tip box with a left accent border and light background. This follows the "info callout" pattern used in Stripe and Tailwind docs — a visually distinct container for supplementary information (tips, warnings, notes) that shouldn't be inline with body text. The left border (3px solid #00d4aa) provides visual hierarchy without relying on background color alone.

Browser compatibility

  • CSS class-based syntax highlighting: Zero compatibility issues — it's just CSS classes on inline elements. Works in every browser back to IE6. The limitation is that you must manually add class names to each token, which doesn't scale for large codebases.
  • navigator.clipboard.writeText(): Requires HTTPS (or localhost). Supported since Chrome 66, Firefox 63, Safari 13.1. Falls back silently — onclick handler can check with navigator.clipboard?.writeText() for graceful degradation.
  • scroll-margin-top: Supported since Chrome 69, Firefox 68, Safari 14.1. On older Safari versions, anchor links jump without offset — the heading sits behind the sticky sidebar. Use an id target element with padding-top as a polyfill.
  • opacity transitions on ::before/::after: The anchor link appears on heading hover. The opacity transition is GPU-accelerated and won't cause jank. Works universally.
  • Monospace font stack: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace — prefers modern coding fonts with ligatures, falls back to system monospace. SF Mono ships with macOS, Fira Code is popular on Linux, Cascadia Code on Windows. The monospace fallback ensures every browser has a usable code font.

Accessibility details

  • Anchor links need visible focus states: The # anchor link only appears on heading hover — keyboard users tabbing through headings won't see it. Add .anchor-link:focus { opacity: 1 } to make it visible on keyboard focus as well.
  • Copy button announcement: The Copy button has no screen reader feedback after copying. Add aria-live="polite" region that announces "Copied to clipboard" after navigator.clipboard.writeText() succeeds.
  • Code block language label: The code header shows the language name ("JavaScript", "Terminal"). This helps screen reader users know what kind of code they're about to hear. Without it, they land in a <pre> block with no context.
  • Syntax highlighting is purely visual: Color-coded tokens provide no benefit to screen reader users. Ensure the raw code text is complete and accurate — don't rely on CSS classes to convey meaning that's missing from the text.
  • TOC heading skip: The TOC has an h3 label ("On this page") which creates a heading in the page outline. Screen reader users navigating by heading can use it as a landmark for the table of contents.
  • Heading id collision: Multiple h2 elements with the same id (e.g., two "Quick Start" sections) break anchor links — the browser scrolls to the first match. Ensure each heading has a unique id.

Common pitfalls

  • Syntax highlighting doesn't auto-scale. Each token needs an explicit CSS class in the HTML. For a production docs site, use Prism.js, Shiki, or highlight.js instead — they parse code and output syntax tokens automatically. The manual approach used here is for learning purposes only.
  • Copy button and clipboard.writeText security. navigator.clipboard requires a user gesture (click event) and HTTPS. An onclick handler on a button satisfies the gesture requirement, but the button must be a real <button> element, not a <div> styled as a button.
  • scroll-margin-top not working in some viewports. If the sticky sidebar is taller on small screens (two-line item labels), the 2rem offset may not be enough. Test with the minimum supported viewport.
  • Anchor link # character visible on mobile. On touch devices, there's no hover state, so the # anchor link is always invisible. Add a @media (hover: hover) check or always show the anchor on mobile with lower opacity.
  • Code block horizontal scroll on mobile. overflow-x: auto creates a scrollbar for long code lines, but on mobile the scrollbar can be hidden (iOS) or hard to use. Keep code examples under 80 characters per line.
  • Inline code contrast on colored backgrounds. The #e74c3c red on #eef2f5 meets contrast requirements, but if you embed inline <code> inside warning/info boxes with different backgrounds, the contrast may drop. Test code elements inside every background variant.

Key concepts

  • Modular type scale — h1 2.25rem → h2 1.5rem → h3 1.15rem → body 1rem. Each step ~1.25×
  • Lead paragraph — 1.15rem with muted color for page summaries
  • CSS class-based syntax tokens — .kw, .fn, .str, .num, .cm, .prop
  • Anchor permalink — position: absolute; left: -1.2em with hover-reveal
  • scroll-margin-top — Anchor offset to prevent sticky sidebar overlap
  • Code block header — Filename/language label + copy button
  • navigator.clipboard.writeText() — Requires HTTPS and user gesture
  • Hierarchical TOC — toc-h2 / toc-h3 nested structure matching heading depth

Next up

Step 4 makes the entire page responsive: collapsible sidebar on mobile, hamburger toggle, mobile TOC, and touch-friendly navigation.

Syntax-highlighted code ▶ Run
<pre><code>
<span class="kw">const</span> agent =
  <span class="kw">new</span>
  <span class="fn">Agent</span>({
  <span class="prop">name</span>:
    <span class="str">'research'</span>,
  <span class="prop">model</span>:
    <span class="str">'gpt-4o'</span>
});
Typography CSS
.content h1 {
  font-size: 2.25rem;
  font-weight: 800;
  letter-spacing: -0.02em;
  line-height: 1.2;
}
.content h2 {
  font-size: 1.5rem;
  margin: 3rem 0 0.75rem;
  border-bottom: 1px solid #e2e6ea;
}
.content h2[id],
.content h3[id] {
  position: relative;
  scroll-margin-top: 2rem;
}
.anchor-link {
  position: absolute;
  left: -1.2em;
  color: #00d4aa;
  opacity: 0;
  transition: opacity 0.15s;
}
.content h2[id]:hover
  .anchor-link {
  opacity: 1;
}
.content pre .kw {
  color: #c792ea;
}
.content pre .str {
  color: #c3e88d;
}
✏️ Type scale tip: The "major third" scale (1.25×) is the most readable for documentation. Start with a base of 16px (1rem), then multiply: 1rem → 1.25rem → 1.563rem → 1.953rem → 2.441rem. Round to sensible values. For production docs, generate the scale with calc() or CSS variables instead of hardcoding each size.
04

Responsive

Complete

Why learn this?

Over 60% of documentation traffic comes from mobile devices — developers reading docs on their phones while coding, on the go, or in meetings. A docs site that works on desktop but breaks on mobile isn't publishable. The challenge: sidebar navigation that's 260px wide on desktop has to collapse into a hamburger menu on mobile without losing the user's place. Astro Docs, Tailwind Docs, and MDN all use the same responsive pattern: a fixed sidebar that slides in from the left on mobile, with a hamburger toggle and a translucent overlay.

Design decisions & tradeoffs

Slide-in sidebar vs toggle visibility. The sidebar uses position: fixed; left: -280px with a CSS transition on left to slide in when the hamburger is clicked. The alternative — display: none / display: block — avoids the transition complexity but feels abrupt. Another alternative — a bottom sheet or drawer — works better for navigation with many items but occupies valuable vertical space on mobile. The slide-in pattern is the most familiar to users (GitHub, Gmail, Slack all use it).

Hamburger button vs visible toggle. The .hamburger button is a 44×44px tap target with three <span> bars that animate into an X when open. The 44px size follows WCAG 2.5.5 (Target Size) recommendation. The animation (transform: rotate(45deg) on the top and bottom bars, opacity: 0 on the middle) is the standard "hamburger to X" transition used by Twitter, GitHub, and Bootstrap. The alternative — moving the sidebar permanently into view on tablet — trades mobile space for convenience on iPad-sized screens.

Overlay backdrop. When the sidebar is open on mobile, a semi-transparent overlay (background: rgba(0,0,0,0.5)) covers the content area. Clicking the overlay closes the sidebar. This pattern prevents users from interacting with hidden content behind the sidebar — a common UX problem with slide-in panels. The overlay uses opacity transition for smooth fade-in, and pointer-events: none when hidden to prevent blocking clicks on the content below.

Body scroll lock. When the sidebar is open, document.body.style.overflow = 'hidden' prevents the background content from scrolling. This is critical on mobile where the content behind a slide-in panel can scroll independently, creating a confusing nested-scroll experience. The alternative — touch-action: none — prevents touch scrolling but doesn't stop keyboard scroll (arrow keys, spacebar). The body scroll lock is reset when the sidebar closes.

Close sidebar on link click. On mobile, clicking a sidebar link navigates to the section and automatically closes the sidebar. This is handled by a querySelectorAll loop that checks window.innerWidth <= 768 and closes the sidebar. Without this, mobile users would navigate to a section and still see the sidebar overlay, blocking the content they just navigated to.

Breakpoint choice. The mobile breakpoint is 768px — the standard "tablet" breakpoint used by Tailwind, Bootstrap, and most CSS frameworks. Below 768px, the sidebar becomes a fixed overlay. Above 768px, it stays as a permanent sticky sidebar. The content area's padding and font sizes also scale down at this breakpoint, with an additional 480px breakpoint for very small phones (iPhone SE, 375px width).

Mobile typography. At 768px, h1 drops from 2.25rem to 1.75rem, h2 from 1.5rem to 1.25rem, and body text from 1rem to 0.95rem. These aren't arbitrary — they maintain the visual hierarchy proportion while fitting the narrower viewport. Code blocks reduce padding and font size, and use negative horizontal margin (margin: 0 -0.5rem) to extend slightly into the page edges, matching the overflow behavior seen on MDN and GitHub READMEs on mobile.

Browser compatibility

  • position: fixed sidebar: Works universally. On iOS Safari, fixed positioning inside a scrollable page can cause visual glitches when the URL bar collapses/expands during scroll. Test with dynamic viewport changes.
  • CSS transitions on left: The left property triggers layout recalculation on every animation frame (not composited). For smoother performance, use transform: translateX() instead — transforms are GPU-accelerated and don't trigger layout. The tradeoff: transform can interfere with position: fixed in some browsers.
  • body.style.overflow = 'hidden': Works universally but has a side effect on iOS Safari: the body "snaps" to the top when overflow is hidden, losing the user's scroll position. Use position: fixed; width: 100% as an alternative on iOS.
  • Hamburger CSS transforms: transform: rotate() and opacity are composited properties — smooth 60fps animation. The translateY() values in the hamburger animation depend on bar thickness; if you change height: 2.5px, adjust the translate values proportionally.
  • 768px breakpoint: The standard "md" breakpoint in Tailwind/Bootstrap. Below this, assume touch interaction and small viewport. Above, assume mouse/keyboard with sufficient horizontal space.

Accessibility details

  • aria-label on hamburger: The hamburger button has aria-label="Toggle navigation" — screen readers announce the button's purpose. The three <span> elements inside are decorative and don't need labels.
  • Focus management on sidebar open: When the sidebar opens, focus should move to the first nav link. When it closes, focus should return to the hamburger button. Without this, keyboard users tab into "nowhere" after closing the sidebar.
  • Overlay click vs keyboard dismiss: The overlay responds to click events but not Escape key. Add a keydown listener for Escape to close the sidebar — this is a WCAG 2.1.1 requirement for keyboard-accessible modal patterns.
  • Close button in sidebar: The × close button in the sidebar gives mouse users an alternative way to close (not just clicking the overlay). It has aria-label="Close navigation".
  • Sidebar link target size: On mobile, min-height: 2.4rem (~38px) on sidebar links ensures they meet the 24px minimum touch target. The desktop size is min-height: 2.2rem. Always increase touch targets on mobile — fingers are less precise than mice.
  • Reduced motion: The sidebar slide animation (transition: left 0.3s ease) can cause motion sickness. Wrap it in @media (prefers-reduced-motion: reduce) { transition: none; } to disable the animation.

Common pitfalls

  • Body scroll snap on iOS. Setting document.body.style.overflow = 'hidden' on iOS Safari scrolls the page to the top. Fix: record window.scrollY before locking, restore it after unlocking, and use position: fixed with top: -scrollY as an alternative.
  • Transition on left vs transform. Animating left triggers layout recalculation (repaints the entire page). transform: translateX() is GPU-composited and doesn't trigger layout. Use transform for slide-in panels when possible.
  • Sidebar content visible on resize. If a user opens the sidebar on mobile (768px width) then resizes their browser to desktop width (900px), the sidebar stays in its "open" state with the overlay visible. Use a matchMedia listener to reset the sidebar state when crossing the 768px threshold.
  • Code blocks wider than viewport. On mobile, code blocks with long lines create horizontal scrollbars inside the content area. The negative margin trick (margin: 0 -0.5rem) helps extend code blocks to the viewport edge but can clip shadows or borders. Test with the longest code example on the narrowest viewport.
  • Z-index stacking with other fixed elements. The sidebar overlay uses z-index: 98, the sidebar uses z-index: 99, and the scroll progress bar uses z-index: 100. If the page had a sticky top announcement bar or cookie consent banner, the z-index layers would need coordination.

Key concepts

  • position: fixed; left: -280px — Off-screen sidebar that slides in on mobile
  • Hamburger to X animation — translateY + rotate on spans, opacity: 0 on middle
  • body.style.overflow = 'hidden' — Scroll lock while sidebar is open. iOS needs position: fixed workaround
  • Overlay backdrop — Prevents interaction with hidden content. Dismisses on click
  • @media (max-width: 768px) — Standard mobile breakpoint. Wide enough for iPad portrait
  • min-height: 2.4rem on mobile nav links — Minimum touch target for fingers
  • prefers-reduced-motion — Disable slide animation for vestibular disorder users
  • window.matchMedia() — Listen for breakpoint crossings to reset sidebar state

All 4 steps complete

The documentation page is fully responsive. Together these 4 steps cover: semantic HTML layout (Step 1), sticky sidebar with scroll tracking (Step 2), typography and code blocks (Step 3), and responsive mobile navigation (Step 4). Ready for the next project: a palindrome checker with JavaScript.

Mobile hamburger ▶ Run
<div class="mobile-header">
  <button class="hamburger"
    id="hamburger"
    aria-label="Toggle nav">
    <span></span>
    <span></span>
    <span></span>
  </button>
  <span>AgentForge Docs</span>
</div>

<div class="sidebar-overlay"
  id="sidebarOverlay">
</div>

<nav class="sidebar" id="sidebar">
  <button class="sidebar-close"
    aria-label="Close">&times;</button>
  ...
</nav>
Responsive CSS
@media (max-width: 768px) {
  .mobile-header {
    display: flex;
  }
  nav.sidebar {
    position: fixed;
    left: -280px;
    z-index: 99;
    height: 100vh;
    width: 280px;
    transition: left 0.3s ease;
  }
  nav.sidebar.open {
    left: 0;
  }
  .sidebar-overlay.open {
    display: block;
  }
  main.content {
    padding: 1.5rem 1.25rem;
  }
  .content h1 {
    font-size: 1.75rem;
  }
}

.hamburger {
  display: flex;
  flex-direction: column;
  gap: 4px;
  background: none;
  border: none;
  padding: 8px;
  min-width: 44px;
  min-height: 44px;
  cursor: pointer;
}
.hamburger span {
  width: 22px;
  height: 2.5px;
  background: #fff;
  transition: transform 0.3s;
}
.hamburger.open span:nth-child(1) {
  transform: translateY(6.5px)
    rotate(45deg);
}
Mobile toggle JS
const hamburger =
  document.getElementById('hamburger');
const sidebar =
  document.getElementById('sidebar');
const overlay =
  document.getElementById('sidebarOverlay');

function toggleSidebar(open) {
  sidebar.classList.toggle('open', open);
  hamburger.classList.toggle('open', open);
  overlay.classList.toggle('open', open);
  document.body.style.overflow =
    open ? 'hidden' : '';
}

hamburger.addEventListener('click',
  () => toggleSidebar(
    !sidebar.classList.contains('open')
  ));

overlay.addEventListener('click',
  () => toggleSidebar(false));

// Close on link click (mobile)
sidebar.querySelectorAll('a')
  .forEach(link => {
    link.addEventListener('click',
      () => {
        if (window.innerWidth <= 768)
          toggleSidebar(false);
      });
  });

// Escape key
document.addEventListener('keydown',
  (e) => {
    if (e.key === 'Escape')
      toggleSidebar(false);
  });
📱 Mobile docs tip: Always test sidebar behavior at exactly 768px width (iPad portrait) AND 375px (iPhone SE). The sidebar close button can overlap with the hamburger button on narrow screens — adjust padding or reposition for the smallest supported viewport. Also add a matchMedia listener to auto-close the sidebar when resizing from mobile to desktop.
Model
DeepSeek V4 Flash
Total Tokens
~30K
Est. Cost
$0.01
Steps
4 / 4
Output tokens measured from 4 demo files (45KB total) × 0.28 tok/byte. Input estimated from session context turns. DeepSeek V4 Flash at ~$0.028/M cached input + $0.07/M output.

✅ Doc Page Complete

All 4 steps built. Ready to move to the next project:

05
Palindrome CheckerString manipulation, JavaScript logic, edge case handling, test-driven development

Lessons Learned — Build Process

The AI challenges, design insights, and pipeline improvements from building Doc Page with AI.

🎮
Why Doc Page Was Popular

Documentation pages are the most information-dense layout on the web. Stripe, Notion, Linear, and every developer tool uses the same pattern: a fixed sidebar navigation, a main content area with headings and code blocks, and often a right-side table of contents. The design challenge is balancing readability with density — code blocks need monospace, prose needs comfortable line lengths, navigation needs to survive scroll. The transferable principle: information hierarchy is a UI pattern. The sidebar says 'here's everything you can learn', the content area says 'here's what you're reading now', and the ToC says 'here's where we are in this page'. Users navigate all three simultaneously.

⚠️
The Problem — AI Shortcomings

The AI generated broken sidebar navigation — clicking a link scrolled past the target because the AI didn't account for the fixed header height (scroll-margin-top was missing). Code blocks were unstyled and overflowed the content area horizontally. The sidebar didn't highlight the current section because the AI used CSS-only solutions that didn't work with scroll-based active tracking.

🛠️
The Fix — Pipeline Improvements

Section anchors now use scroll-margin-top: 6rem to account for the fixed header. Code blocks get overflow-x: auto and a monospace stack. Sidebar active tracking was replaced with an IntersectionObserver-based approach in JS (the AI's CSS-only approach couldn't handle multiple simultaneous sections in view). These patterns are now standard for all documentation-style pages.

Each app builds on the last. The bugs found in documentation were fixed before the next app was built — and every bug saves time on every future app.