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.
:root {
--dot-green: #7ee787; /* terminal green — primary */
--dot-green-bright: #b0f5b7; /* hover / focused */
--dot-green-dim: #4a9153; /* muted */
--dot-bg: #0b0e14; /* near-black base */
--dot-bg-elev: #111621; /* elevated card */
--dot-fg: #e4e7ec;
--dot-fg-muted: #94a3b8;
--dot-border: #1f2937;
}
[data-md-color-scheme="slate"] {
--md-default-bg-color: var(--dot-bg);
--md-default-fg-color: var(--dot-fg);
--md-primary-fg-color: var(--dot-green);
--md-accent-fg-color: var(--dot-green-bright);
--md-typeset-a-color: var(--dot-green);
--md-code-bg-color: #161b26;
}
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 */
.md-header {
background-color: rgba(11, 14, 20, 0.92);
backdrop-filter: saturate(180%) blur(12px);
border-bottom: 1px solid var(--dot-border);
}
/* Grid cards on landing page — hover lift + accent glow */
.md-typeset .grid.cards > :is(ul, ol) > li {
background: var(--dot-bg-elev);
border: 1px solid var(--dot-border);
border-radius: 12px;
transition: transform 180ms ease, border-color 180ms ease;
}
.md-typeset .grid.cards > :is(ul, ol) > li:hover {
transform: translateY(-2px);
border-color: rgba(126, 231, 135, 0.35);
box-shadow: 0 8px 24px -8px rgba(0, 0, 0, 0.5);
}
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:
---
hide:
- navigation
- toc
---
<section class="dot-hero" markdown>
# .dotfiles
<p class="tagline">Cross-platform, signed, local-first dotfiles…</p>
<div class="buttons">
<a class="primary" href="guides/INSTALL/">Install →</a>
<a href="https://github.com/sebastienrousseau/dotfiles">GitHub</a>
</div>
</section>
## What's inside
<div class="grid cards" markdown>
- :material-console:{ .lg .middle } **Multi-shell parity**
---
Bash, Zsh, Fish, Nushell — same aliases, functions, prompt, and completions.
[→ Shell hub](https://doc.dotfiles.io/reference/UTILS/)
</div>
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.