Articles
Custom Documentation Sites in 2026: Building a Distinctive Dark-Themed Developer Reference on MkDocs Material Without Forking
Documentation is the public API surface of an open-source project; the difference between the default Material theme and a bespoke palette is the difference between "reads like every other project" and "reads like this specific project".
Sebastien Rousseau · Published 5 Jul 2026 · 11 min read
Why Documentation Aesthetics Matter in 2026
An open-source project's documentation site is the first surface an evaluator touches — before the README, before the release notes, before the code. In 2026, the density of developer-tool competition means that visual differentiation carries measurable weight: does the site feel like a curated product, or does it feel like a Jekyll-Cayman default from 2019? The felt distinction shapes adoption decisions before the reader has read a single sentence.
The reference standard for "distinctive open-source documentation" is docs.n8n.io — dark, polished, opinionated, immediately identifiable as n8n's. n8n runs on GitBook, a commercial SaaS. Most open-source projects, .dotfiles included, cannot justify GitBook's licensing but can invest in MkDocs Material customisation to achieve equivalent visual distinction on an open-source stack.
This article documents the migration of doc.dotfiles.io from Jekyll's Cayman theme (the default when GitHub Pages serves markdown without a MkDocs configuration) to a fully customised MkDocs Material theme with a terminal-green-on-near-black palette, custom typography, and a hero + card-grid landing page.
The Custom Docs Theme 2026 Architecture Lens
MkDocs Material's customisation surface has four distinct layers, each with different capabilities and constraints:
| Layer | Design Decision | Why It Matters | Risk if Mishandled |
|---|---|---|---|
| Palette declaration | mkdocs.yml theme.palette.primary: custom + accent: custom | Signals to MkDocs Material that CSS custom properties will drive the palette rather than a named preset (green, teal, indigo, etc.) | Named presets constrain the palette to Material Design's colour tokens; custom unlocks arbitrary hex values via CSS variables |
| CSS custom-property overrides | docs/stylesheets/extra.css sets --md-primary-fg-color, --md-accent-fg-color, --md-default-bg-color, --md-code-bg-color under [data-md-color-scheme="slate"] | The single source of truth for the palette. Every Material component reads from these variables | Overriding component-level CSS rules rather than variables creates unmaintainable per-component drift as MkDocs Material upgrades |
| Component restyling | Same extra.css overrides selectors like .md-header, .md-nav__link, .grid.cards > ul > li | Where the "feels bespoke" work happens — spacing, borders, hover states, gradients, backdrop blur | Under-styling produces "Material Design in a different colour"; over-styling drifts away from Material's ergonomic defaults |
| Landing-page markup | docs/index.md with Front-Matter hide: [navigation, toc] + <section class="dot-hero"> + <div class="grid cards" markdown> | The homepage is the highest-impact surface; it should not look like a table of contents | Default MkDocs Material index reads like documentation; a custom hero reads like a product page |
Key Documentation-UX Signals
| Signal | Operational Benchmark | Reference | Technical Platform Implementation |
|---|---|---|---|
| Time-to-first-CTA | Hero action button visible above the fold in ≤ 100 ms of first-paint | Landing-page conversion norms | Custom hero section with primary and secondary CTAs immediately below the site title |
| Palette Distinctiveness | Primary accent color is unique to the project, not a Material Design preset | Brand recognition | CSS custom property overrides on [data-md-color-scheme] selectors |
| Reading Contrast | WCAG AA compliance on all text-on-background pairs | Accessibility gate | --md-default-fg-color (#e4e7ec) on --md-default-bg-color (#0b0e14) = 15.6:1 ratio |
| Cognitive Load per Section | Feature-card grid on landing page (visual chunking) rather than a bulleted link list | Landing-page ergonomics | <div class="grid cards" markdown> + 8 cards with material icons |
| Build Reproducibility | mkdocs.yml + docs/stylesheets/extra.css under version control; hashes locked in requirements-docs.txt | Supply-chain hygiene | pip-compile --generate-hashes + pip install --require-hashes in the Pages workflow |
| Cache Cost at Edge | CDN TTL respected; theme changes propagate to doc.<domain> within 10 minutes | Deployment latency | Cloudflare (or equivalent) max-age=600 on the site |
Diagnosis: What "Default MkDocs Material" Leaves on the Table
An out-of-the-box MkDocs Material site with primary: teal, accent: teal and no extra_css is visually acceptable — but it is one of many thousand acceptable sites that look identically acceptable. The named presets are constrained to Google's Material Design palette; the sidebar, header, and content surface all read as "Material default".
For a project positioning itself as "an opinionated developer platform, not a library", the visual signal that the docs are the product's docs — not somebody else's — is a marketing surface, not a decoration. The lift is contained to three files:
mkdocs.yml— palette selector configurationdocs/stylesheets/extra.css— the actual palette + typography + component overridesdocs/index.md— hero + card grid replacing the default index
No template overrides (custom_dir), no plugin authoring, no JavaScript. All the customisation lives in files MkDocs Material is explicitly designed to consume.
Remediation: The Three-File Custom Theme
mkdocs.yml Palette Configuration
The magic value that unlocks CSS-driven colours is primary: custom (and accent: custom). Under [data-md-color-scheme="slate"], MkDocs Material's dark variant, every component reads its colours from CSS custom properties that we get to define.
theme:
name: material
font:
text: Inter
code: JetBrains Mono
palette:
- media: "(prefers-color-scheme: dark)"
scheme: slate
primary: custom
accent: custom
- media: "(prefers-color-scheme: light)"
scheme: default
primary: custom
accent: custom
features:
- navigation.tabs
- navigation.tabs.sticky
- navigation.footer
- content.code.copy
extra_css:
- stylesheets/extra.css
The navigation.tabs.sticky feature keeps the top-level navigation visible on scroll, which pairs with the hero + card grid to keep the site feeling like a product page rather than a scrolling article.
docs/stylesheets/extra.css — Palette + Component Overrides
The stylesheet is organised in five zones: CSS custom properties for the palette, typography, dark-scheme palette overrides, per-component restyling, and the custom hero + grid-card styles.
}
}
The choice of #7ee787 (GitHub's terminal green) as the accent is deliberate: it reads as "developer tool" to the target audience without being GitHub-branded, and the WCAG contrast on the #0b0e14 background exceeds 12:1 for text and 4.5:1 for the accent-on-background — comfortably above AA thresholds.
Component overrides are targeted at the highest-impact surfaces:
/* Header: blurred backdrop-saturate for a floating feel */
}
/* Grid cards on landing page — hover lift + accent glow */
) >}
) >}
docs/index.md — Hero + Grid Cards
The landing page is not documentation — it is a product surface. MkDocs Material's md_in_html extension allows Markdown to nest inside a custom HTML section:
- -
-
Bash, Zsh, Fish, Nushell — same aliases, functions, prompt, and completions.
[→ Shell hub](https://doc.dotfiles.io/reference/UTILS/)
The hide: [navigation, toc] front-matter removes the sidebar and right-column table-of-contents on this page only — the landing page gets the full canvas, sub-pages retain the standard docs layout.
Verification: Build, Deploy, Contrast
The custom theme adds ~12 KB of CSS to the built site. Local mkdocs build --clean completes in ~2 seconds. The Pages workflow uses hash-locked dependencies (pip install --require-hashes -r requirements-docs.txt) to keep the build reproducible across MkDocs Material and its 30+ transitive dependencies.
Accessibility contrast measured on the deployed site:
- Body text (
#e4e7econ#0b0e14) — 15.6:1 (WCAG AAA) - Accent (
#7ee787on#0b0e14) — 12.4:1 (WCAG AAA) - Muted text (
#94a3b8on#0b0e14) — 7.2:1 (WCAG AAA)
Cloudflare CDN cache invalidation after Pages deploy: ~10 minutes on max-age=600. Fetching the GitHub Pages origin (sebastienrousseau.github.io/dotfiles/) reflects the new theme immediately; the CDN-fronted custom domain propagates within one cache TTL.
Return on Resilience
| Metric | Before (Jekyll Cayman) | After (Custom MkDocs Material) |
|---|---|---|
| Page weight | 2.9 KB | 14.8 KB (compressed 4.2 KB) |
| Time to visible hero | ~800 ms (no hero) | ~150 ms |
| WCAG AA compliance | Passes body text; hero absent | Passes AAA on all text pairs |
| Landing-page CTAs | 0 (link list only) | 4 (primary + 3 secondary) |
| Visual differentiation from default | Zero | Distinctive palette + hero + grid |
| Build reproducibility | Jekyll on github-pages gem (unpinned transitive deps) | pip install --require-hashes (fully locked) |
Takeaways
-
primary: custom+extra_cssbeats theme forking. Nocustom_dir, no Jinja templates, no plugin authoring. All the customisation lives in files MkDocs Material is explicitly designed to consume. -
CSS custom properties are the maintainable seam. Overriding
--md-primary-fg-colorscales; overriding.md-header a.md-header__button:not(...)selectors doesn't. -
Treat the landing page as a product surface, not documentation. Front-matter
hide: [navigation, toc]unlocks the full canvas. Hero + card grid + tabbed quick-start reads as a product page. -
Lock the docs-build supply chain.
pip-compile --generate-hashes+pip install --require-hashescloses the ScorecardPinnedDependenciesIDalert and prevents transitive-dep drift in Pages deploys. -
Measure contrast, not just aesthetics. WCAG AAA on body text is achievable with the right palette; it's not a tradeoff against distinctiveness.
The reference implementation landed as PR #960 and is live at doc.dotfiles.io; the theme source lives at docs/stylesheets/extra.css.