# dioxus-docs-kit
Reusable documentation site kit for [Dioxus](https://dioxuslabs.com/) applications.
Drop-in layout with sidebar navigation, full-text search, page navigation, OpenAPI API reference pages, mobile drawer, and theme toggle. Built on [dioxus-mdx](https://crates.io/crates/dioxus-mdx) for content rendering.
## Quick Start
Add to your `Cargo.toml`:
```toml
[dependencies]
dioxus-docs-kit = "0.7"
```
### 1. Build a Registry
The `DocsRegistry` holds all parsed content, the navigation tree, the search index, and any OpenAPI specs:
```rust
use dioxus_docs_kit::{DocsConfig, DocsRegistry};
use std::sync::LazyLock;
include_str!("../docs/_nav.json"),
doc_content_map(), // HashMap<&'static str, &'static str> generated by build.rs
)
.with_openapi("api-reference", include_str!("../docs/api-reference/spec.yaml"))
.build()
});
```
### 2. Wire Into Your Router
Create a thin layout wrapper that provides the `DocsContext` and registry to the library components. The `use_docs_providers` hook bundles all the context setup into one call:
```rust
use dioxus::prelude::*;
use dioxus_docs_kit::{DocsLayout, use_docs_context, use_docs_providers};
#[component]
fn MyDocsLayout() -> Element {
let nav = use_navigator();
let route = use_route::<Route>();
// A plain String — extract the slug from your route, e.g. slug.join("/").
let current_path = /* ... */;
// `use_docs_context` rewraps the path reactively for you. Building the
// signal yourself with a plain `use_memo(move || ...)` captures the first
// route and never updates — the sidebar highlight would freeze.
let docs_ctx = use_docs_context(
current_path,
"/docs",
Callback::new(move |path: String| {
nav.push(/* build route from path */);
}),
)
.with_site_url("https://your-site.com"); // optional: canonical/OG URLs
let providers = use_docs_providers(&DOCS, docs_ctx);
// providers.search_open / providers.drawer_open are available for
// wiring a custom header.
rsx! {
DocsLayout {
Outlet::<Route> {}
}
}
}
```
### 3. Add Routes
```rust
#[derive(Routable, Clone, PartialEq)]
enum Route {
#[layout(MyDocsLayout)]
#[route("/docs")]
DocsIndex {},
#[route("/docs/:..slug")]
DocsPage { slug: Vec<String> },
}
```
That's it. The library handles sidebar rendering, search, page content, previous/next navigation, and mobile responsiveness.
## Components
| `DocsLayout` | Full page layout with sidebar, content area, and table of contents |
| `DocsSidebar` | Navigation sidebar built from `_nav.json` |
| `DocsPageContent` | Renders MDX docs or OpenAPI endpoint pages |
| `DocsPageNav` | Previous/next page navigation |
| `SearchModal` | Full-text search across all docs |
| `SearchButton` | Trigger button for the search modal |
| `MobileDrawer` | Mobile navigation drawer |
| `ThemeToggle` | Light/dark theme switcher |
## Navigation Config
Define your docs structure in `_nav.json`. `tabs` is a flat list of tab names, and each group optionally names the tab it belongs to:
```json
{
"tabs": ["Docs", "Guides"],
"groups": [
{
"group": "Getting Started",
"tab": "Docs",
"pages": [
"getting-started/introduction",
"getting-started/quickstart"
]
}
]
}
```
## Content Pipeline
All doc content is embedded at compile time via `include_str!()`. A typical `build.rs` reads `_nav.json`, collects all referenced `.mdx` files, and generates a `HashMap<&'static str, &'static str>` mapping paths to content.
See the [example project](https://github.com/hauju/dioxus-docs-kit) for a complete `build.rs` implementation.
## Styling Setup
### Zero-setup: the precompiled stylesheet
The crate ships a compiled stylesheet (`DOCS_KIT_CSS`) covering every class its
components emit — Tailwind utilities, DaisyUI dark/light themes, typography
prose, and the `--dk-*` theming tokens. Link it and skip the rest of this
section — no Tailwind, no Bun, no safelist:
```rust
rsx! {
document::Stylesheet { href: dioxus_docs_kit::DOCS_KIT_CSS }
}
```
It only contains the *kit's* classes; if your own pages use Tailwind utilities
the kit doesn't, run your own build instead. That path requires **Tailwind CSS
4**, **DaisyUI 5**, and **@tailwindcss/typography**:
### Install dependencies
```sh
bun add tailwindcss @tailwindcss/typography daisyui
```
### Configure Tailwind
Add to your `tailwind.css`:
```css
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@plugin "daisyui" {
themes: dark --default, light;
}
@source "./src/**/*.{rs,html,css}";
```
### Include dioxus-docs-kit classes
When using as a **crates.io dependency**, Tailwind can't scan the crate source
(it lives in `~/.cargo` with machine-specific paths). Copy `safelist.html` from the
crate into your project root and add it as a source:
```css
@source "./safelist.html";
```
The safelist includes all classes from both `dioxus-docs-kit` and `dioxus-mdx`, including
dynamic runtime classes that Tailwind cannot detect from source scanning alone.
When using as a **workspace path dependency**, you can point directly at the source instead:
```css
@source "./crates/dioxus-docs-kit/src/**/*.rs";
@source "./crates/dioxus-mdx/src/**/*.rs";
```
## Theming
The kit exposes a public theming surface so consumers can restyle without
forking or fighting DaisyUI internals.
### Public CSS variables
Copy `theme.css` from the crate into your project and import it alongside
Tailwind:
```css
@import "tailwindcss";
@import "./theme.css";
```
All tokens have DaisyUI fallbacks, so DaisyUI users inherit the current theme;
non-DaisyUI users get a sensible neutral default. Override any of them:
```css
.my-site .dk-root {
--dk-accent: #f0a57c;
--dk-font-heading: 'Instrument Serif', serif;
--dk-radius-lg: 20px;
}
```
| `--dk-bg`, `--dk-bg-sub`, `--dk-bg-alt` | Surface colors |
| `--dk-fg`, `--dk-muted`, `--dk-dim` | Foreground / text colors |
| `--dk-border` | Border color |
| `--dk-accent`, `--dk-accent-fg`, `--dk-accent-soft` | Accent |
| `--dk-radius-sm`, `--dk-radius`, `--dk-radius-lg` | Corner radii |
| `--dk-font-body`, `--dk-font-heading`, `--dk-font-mono` | Typography |
| `--dk-article-width`, `--dk-sidebar-width`, `--dk-toc-width` | Layout widths |
### Stable `dk-*` classes
Structural nodes carry semver-stable `dk-*` class names. Target these in your
own CSS instead of DaisyUI or internal classes:
| `dk-root`, `dk-docs-root` | Outermost docs wrapper |
| `dk-header`, `dk-shell`, `dk-sidebar`, `dk-main`, `dk-toc` | Layout regions |
| `dk-tabs`, `dk-tab`, `dk-tab-active` | Tab bar |
| `dk-nav`, `dk-nav-group`, `dk-nav-group-title`, `dk-nav-item`, `dk-nav-item-active` | Sidebar navigation |
| `dk-article`, `dk-article-header`, `dk-article-title`, `dk-article-description`, `dk-article-body` | Article regions |
| `dk-pagination`, `dk-page-prev`, `dk-page-next` | Previous/next links |
| `dk-search-trigger`, `dk-search-dialog`, `dk-search-input`, `dk-search-results`, `dk-search-result` | Search |
| `dk-drawer` | Mobile drawer |
### Slots on `DocsLayout`
`DocsLayout` accepts optional element slots. Each slot is wrapped in a
`dk-*-slot` class for CSS hooks.
```rust
DocsLayout {
announcement_bar: Some(rsx!{ AnnouncementBar {} }),
sidebar_header: Some(rsx!{ MyProductSwitcher {} }),
sidebar_footer: Some(rsx!{ EditOnGitHub {} }),
footer: Some(rsx!{ SiteFooter {} }),
Outlet::<Route> {}
}
```
`DocsPageContent` additionally accepts an `article_footer` slot (rendered
below the article body, before pagination) — useful for "Was this helpful?"
widgets.
### Density variants
`DocsLayout` accepts a `variant` prop for two built-in density presets:
```rust
DocsLayout { variant: DocsVariant::Reference, Outlet::<Route> {} }
```
| `Prose` (default) | Wide margins, serif-friendly, long-form reading | `72ch` |
| `Reference` | Tighter column, smaller type, denser headings | `64ch` |
The variant is emitted as a class on `dk-root` (`dk-variant-prose` or
`dk-variant-reference`) so consumers can layer further tweaks.
### Pre-built theme examples
`examples/themes/` ships three drop-in visual identities built entirely on
top of `--dk-*` tokens:
- `warm-editorial.css` — amber accent, serif headings, cream surfaces
- `brutalist-light.css` — high contrast, zero-radius, monospace
- `default.css` — baseline (nothing overridden)
See [THEMING.md](./THEMING.md) for the full roadmap and proposals open for
community contribution.
## Features
- `web` (default) — enables web-specific features (propagated to `dioxus-mdx`)
- `mermaid` (default) — renders ` ```mermaid ` fences as diagrams
- `highlight` (default) — syntax-highlights code blocks via [`dioxus-code`](https://crates.io/crates/dioxus-code). Disable (`default-features = false`) to drop the dependency and its C-compiling tree-sitter grammars: no C toolchain (or wasm `stderr` shim) is needed and the binary is smaller, but code blocks render as plain (uncolored) text. Turning it off also removes the `dioxus-code` re-exports and `DocsConfig::with_code_theme[s]`.
- `lang-*` — one tree-sitter grammar each, on top of `highlight`. Enabled by default: `lang-bash`, `lang-css`, `lang-dockerfile`, `lang-html`, `lang-javascript`, `lang-json`, `lang-markdown`, `lang-python`, `lang-toml`, `lang-typescript`, `lang-yaml`. Also available: `lang-rust` (a no-op — Rust always highlights), `lang-c-sharp`, `lang-cpp`, `lang-tsx`. Any other language goes through `dioxus-code` directly; see [Syntax Highlighting](#syntax-highlighting).
- `openapi` (default) — renders API reference pages from OpenAPI specs. Required by `DocsConfig::with_openapi()`, which is absent without it. Disable (`default-features = false`) to drop `openapiv3` and `serde_yaml` (and its `unsafe-libyaml`) from the build; docs pages, blog and frontmatter are unaffected.
- `server` — Axum route builders for crawler-facing endpoints
With the `server` feature, `SeoRouter` generates per-page raw-Markdown routes, `llms.txt` / `llms-full.txt`, sitemaps, blog RSS, and robots.txt as plain Axum routes (server functions would JSON-encode the bodies):
```rust
use dioxus_docs_kit::server::SeoRouter;
.with_docs(&DOCS, "/docs")
.into_router();
Ok(dioxus::server::router(App).merge(seo))
});
```
## Syntax Highlighting
Code blocks render through [`dioxus-code`](https://crates.io/crates/dioxus-code). Every language is a separate tree-sitter grammar compiled into your binary, so each one sits behind its own `lang-*` feature. Enabled by default:
`bash`, `css`, `dockerfile`, `html`, `javascript`, `json`, `markdown`, `python`, `rust`, `toml`, `typescript`, `yaml`
Rust needs no feature of its own — `dioxus-code`'s `runtime` always compiles it, so `highlight` alone highlights Rust.
`lang-c-sharp`, `lang-cpp` and `lang-tsx` exist too but are deliberately **off** by default: dropping the C# and C++ grammars cut this repo's own release wasm from 19.0 MB to 9.9 MB (data section 15.2 MB to 6.3 MB), and TSX (1.5 MB, React-only) followed. Turn them back on if your docs fence those languages:
```toml
[dependencies]
dioxus-docs-kit = { version = "0.7", features = ["lang-c-sharp", "lang-cpp", "lang-tsx"] }
```
To trim the set down to what your docs actually fence:
```toml
[dependencies]
dioxus-docs-kit = { version = "0.7", default-features = false, features = [
"web", "mermaid", "highlight", "openapi", "lang-bash", "lang-json", "lang-toml",
] }
```
### Any other language
The kit carries `lang-*` features only for the languages above. For anything else `dioxus-code` supports — Go, Zig, Kotlin, SQL, and ~90 more — add `dioxus-code` to your own `Cargo.toml` with the grammar you want. Cargo unifies that feature into the same `dioxus-code` the kit already depends on, so `Language::from_slug` (which the kit uses to resolve a fence) is compiled with the union of the features and picks the grammar up. No kit changes needed:
```toml
[dependencies]
dioxus-docs-kit = "0.7"
dioxus-code = { version = "0.1", default-features = false, features = ["runtime", "lang-go"] }
```
That route matches on `dioxus-code`'s canonical language slug, so use it in the fence (` ```go `). The friendly aliases (` ```c++ `, ` ```yml `, ` ```sh `) only exist for the languages the kit has its own feature for. See the [dioxus-code feature list](https://github.com/DioxusLabs/dioxus-code/blob/main/Cargo.toml) for every available flag.
A fence whose grammar is not compiled into the build renders as plain, uncolored text in the same markup — it does not fail or fall back to an unrelated grammar.
## Production build
Bundle with debug symbols off:
```sh
dx bundle --web --release --debug-symbols false
```
`dx` keeps DWARF by default and `wasm-opt` aborts on it ("compile unit size was incorrect"), after which `dx` silently ships the *unoptimized* wasm-bindgen output — for this repo's own site that was 25 MB instead of 7 MB. Two more things worth copying from this repo's root `Cargo.toml` and CI workflow:
- `[profile.wasm-release]` / `[profile.server-release]` — the profiles `dx` actually builds with (`opt-level = "z"`, fat LTO, `panic = "abort"` for the wasm; `strip = true` for the server).
- Brotli sidecars: write a `.br` next to every `.wasm`/`.js`/`.css` under the bundle's `public/` directory (`brotli -q 11 -k`). `dioxus-server` serves them automatically with `content-encoding: br`, taking the wasm from ~7 MB to ~1.4 MB on the wire. `scripts/wasm-size.sh` in this repo reports both numbers and can gate CI on the raw size.
## License
MIT