The Tailwind/MkDocs Material Bridge: A Surgical CSS Pattern
Running Tailwind CSS components inside a MkDocs Material documentation site introduces a deceptively subtle conflict. This post documents the architectural decision, the failure mode it resolves, and why we chose a pure-CSS solution over the obvious alternatives.
The Failure Mode¶
MkDocs Material applies font-size: 125% to the <html> element globally. This is a deliberate, documented accessibility decision: it scales the effective base unit from 16px to 20px, which improves legibility for users with larger system font preferences.
Tailwind CSS builds every spacing, typography, and sizing value on rem. The result is predictable: every Tailwind component inherits a 25% inflation. p-4 renders at 20px instead of 16px. text-sm measures 17.5px instead of 14px. The landing page layout, designed to a 16px grid, becomes geometrically wrong in every dimension that uses rem.
Fixed px values are immune — max-w-[1400px] works correctly. But that is not a workable escape hatch for a utility-first framework.
The Options We Rejected¶
Global reset. Resetting font-size: 100% on <html> everywhere would fix the landing page and simultaneously break every documentation page on the site — sidebar font sizing, admonition scale, table density, code block metrics. Not viable.
Convert Tailwind to px. This defeats the entire value proposition of a utility framework. ~3,000 utility classes would need per-site overrides. Unmaintainable.
Per-class !important overrides. Same surface area problem as above.
Server-side body class. MkDocs Material supports extra.body_class in page frontmatter. Adding a per-page variable creates a template coupling: the Jinja2 override must now read page metadata to decide whether to apply a class. The CSS fix becomes load-bearing documentation.
The Bridge¶
The solution uses a single CSS :has() rule scoped to a semantic anchor class:
The class zz-tailwind-root is applied to the outermost <div> in overrides/home.html:
zz-tailwind-root has no visual definition. It is purely a signal. When the DOM contains this class, the bridge activates. When it does not — every regular documentation page — the MkDocs Material default is fully preserved.
Why :has() Is the Right Primitive¶
The CSS :has() relational pseudo-class allows a parent element selector to depend on the presence of a descendant. When the <html> element's subtree contains .zz-tailwind-root, the rule fires. Otherwise, it is a no-op with zero specificity impact on unrelated pages.
This is:
- Scoped without coupling to server state
- Pure client-side — no Jinja2 logic, no TOML metadata, no JavaScript
- Zero regression surface — the rule cannot affect pages that do not opt in
- Browser-native in all evergreen engines since mid-2023
Dark Mode¶
The dark: Tailwind variant is functionally inert in this host. MkDocs Material never sets a dark class on <html> — it uses the data-md-color-scheme attribute on <body> instead. All dark-mode-aware Tailwind styles must be written as explicit CSS targeting [data-md-color-scheme="slate"] in extra.css.
This is not a limitation; it is a clean architectural boundary. The MkDocs Material theme owns the colour scheme toggle. The Tailwind components observe it through the same attribute the rest of the site uses.
What This Enables¶
With the bridge in place, the landing page Jinja2 partials can use standard Tailwind utility classes at their designed scale. The 14 ADR ghost entries in the developer nav have been purged in this same commit. The dual-palette configuration now exposes the MkDocs Material theme toggle in the header.
The full technical specification — including the file map, the dark mode sync pattern, and a comparison table of rejected alternatives — is in the developer documentation: Tailwind/MkDocs Material Bridge