Sidebar
How the sidebar navigation is structured and configured.
Item Ordering
Pages within a category are sorted by the sidebar_position frontmatter field. Lower values appear first.
Pages without sidebar_position default to 999 and appear at the end of the category, sorted alphabetically by filename.
---
title: My Page
sidebar_position: 3
---Tip
Always set sidebar_position explicitly to control page order. Without it, pages are sorted alphabetically, which may not match your intended reading order.
Category Ordering
Categories in the sidebar are ordered by the sidebar_position of their index page (index.mdx). If a category has no index page or no sidebar_position, it falls back to alphabetical ordering.
For example, to make "Getting Started" appear first and "Guides" second:
getting-started/index.mdx → sidebar_position: 0
guides/index.mdx → sidebar_position: 1
reference/index.mdx → sidebar_position: 2 Category Index Pages
Creating an index.mdx inside a directory gives the category a landing page. The category name in the sidebar becomes a clickable link to this page.
A typical category index page uses the <CategoryNav /> component to automatically list all pages in the category:
---
title: Guides
sidebar_position: 1
---
Explore our guides to learn about zudo-doc features.
<CategoryNav category="guides" />The <CategoryNav /> component renders a grid of links to all pages in the category (excluding the index itself), sorted by sidebar_position.
Without an index.mdx, the category heading is a toggle-only control — clicking it expands or collapses the category, but does not navigate anywhere.
Default Open/Closed
Categories containing the current page always open. When sidebar_initial_expansion is omitted, ordinary categories retain the existing expanded default; explicitly configured TypeScript collapsed values remain exceptions.
Set sidebar_initial_expansion: collapsed or expanded on a category's index.mdx to supply an inherited baseline. The nearest category index applies, and a field on the current page overrides it. This controls disclosure, while initialPath continues to control the initial sidebar scope.
---
title: Guides
sidebar_position: 1
sidebar_initial_expansion: collapsed
---Only explicit expand and collapse toggles by the reader are saved for the session. Automatically opening the current page's ancestors (on arrival, including Back/Forward and soft navigation, at any viewport width) and opening the root of a focused or broadened scope are not saved. The reader can collapse even the category containing the current page: it stays collapsed for that page, and hash changes, filter typing, and a reload of the same page do not reopen it. Navigating to another page reveals that page's path again, while choices in unrelated branches are kept.
Restore resets disclosure and scope to the current page's author baseline. Restore appears only when those effective values differ. At the highest scope the entire toolbar disappears, while each category's branch-focus control remains available.
Note
Saved state from earlier versions mixed automatic and manual expansion. On the first load after upgrading, that earlier expand/collapse state is discarded once; a saved scope and the filter text are kept. This does not repeat.
Sidebar Label
Use the sidebar_label frontmatter field to display a different label in the sidebar than the page title. This is useful when the full title is too long for the sidebar.
---
title: Getting Started with zudo-doc Installation
sidebar_label: Installation
sidebar_position: 2
---Tip
The sidebar_label only affects the sidebar. The page title and heading still use title.
Directory Structure
Directories inside src/ become sidebar categories. The directory name is converted from kebab-case to Title Case automatically.
src/content/docs/
getting-started/ → "Getting Started"
index.mdx → category index (sidebar_position: 0)
introduction.mdx → sidebar_position: 1
installation.mdx → sidebar_position: 2
writing-docs.mdx → sidebar_position: 3
guides/ → "Guides"
index.mdx → category index (sidebar_position: 1)
configuration.mdx → sidebar_position: 1
frontmatter.mdx → sidebar_position: 2 Nested Categories (Sub-Groups)
Subdirectories within a category directory create nested collapsible groups in the sidebar. Each sub-directory needs an index.mdx file with category_no_page: true to set its label and position without creating a route:
src/content/docs/
methodology/
index.mdx
architecture/
index.mdx → title: Architecture, sidebar_position: 1, category_no_page: true
bem-strategy.mdx
component-first.mdx
design-systems/
index.mdx → title: Design Systems, sidebar_position: 2, category_no_page: true
tight-token.mdx
two-tier-size.mdx With auto-generated sidebar, this renders as 3 levels deep:
Methodology (depth 0)
▸ Architecture (depth 1, collapsible)
BEM Strategy (depth 2)
Component First (depth 2)
▸ Design Systems (depth 1, collapsible)
Tight Token (depth 2)
Two-Tier Size (depth 2)Flattening with Explicit Sidebar Config
For better readability, use explicit sidebar configuration to promote sub-groups to the top level. This reduces nesting by one level. A configuration file is not auto-consumed: bind it through the sidebarsConfig chrome slot.
Requires a sidebar-data binding
sidebarsConfig defaults to {}, so every section uses filesystem auto-generation. Point chromeBindingsModule at a module that binds your config. Fresh route stubs already consume it; do not edit them. SeeCustom Components and Host Chrome Bindings.
Create a config whose keys exactly match categoryMatch values in your headerNav, then bind it:
import type { SidebarsConfig } from "@takazudo/zudo-doc/sidebar-utils";
const sidebars: SidebarsConfig = {
guides: [
"guides/introduction",
{ type: "doc", id: "guides/sidebar", label: "Sidebar" },
{ type: "link", label: "Project site", href: "https://example.com" },
{
type: "category",
label: "Architecture",
collapsed: true,
sortOrder: "asc",
items: [{ type: "autogenerated", dirName: "guides/architecture" }],
},
],
};
export default sidebars;The string form is a document id shorthand. The complete item vocabulary is a string "doc/slug", { type: "doc", id, label? }, { type: "link", label, href }, { type: "category", label, items, collapsed?, sortOrder? }, or { type: "autogenerated", dirName? }. A section without a matching key falls back to filesystem auto-generation.
import { defineChromeBindings } from "@takazudo/zudo-doc/chrome-bindings";
import sidebars from "./sidebars";
export const chromeBindings = defineChromeBindings({
sidebarsConfig: sidebars,
});export default defineConfig(
zudoDoc({
chromeBindingsModule: "./src/chrome-bindings.ts",
}),
);This repository's src/ is a showcase-only reference file. Its exported object is currently empty, so this showcase intentionally uses filesystem auto-generation for every section.
const sidebars: SidebarsConfig = {
methodology: [
"methodology",
{
type: "category",
label: "Architecture",
items: [{ type: "autogenerated", dirName: "methodology/architecture" }],
},
{
type: "category",
label: "Design Systems",
items: [{ type: "autogenerated", dirName: "methodology/design-systems" }],
},
],
};
This renders as 2 levels:
Methodology (depth 0, leaf link)
▸ Architecture (depth 0, collapsible)
BEM Strategy (depth 1)
Component First (depth 1)
▸ Design Systems (depth 0, collapsible)
Tight Token (depth 1)
Two-Tier Size (depth 1)
Key points:
The string
"methodology"references the category index page as a leaf link (children are stripped automatically — the explicit config handles structure)type: "autogenerated"withdirNamepulls articles from a specific subdirectorySub-group categories render at depth 0 with bold labels and border separators
Articles render at depth 1 — one level flatter than auto-generated
Tip
This pattern is recommended when categories have sub-groups. The explicit config keeps the filesystem organized while making the sidebar more readable.
Sort Order (Ascending / Descending)
By default, items within a category are sorted in ascending order (lowest sidebar_position first, then alphabetical). You can reverse this to descending order per category using the category_sort_order frontmatter field on the category's index.mdx:
---
title: Changelog
sidebar_position: 10
category_sort_order: "desc"
---When category_sort_order is "desc", items are sorted in reverse — highest position first, then reverse alphabetical. This is useful for date-based content (changelogs, release notes) where the newest items should appear at the top.
Note trays use the same field as their single ordering contract. The selected direction controls how stable ranks are listed in the sidebar, <NoteTrayIndex>, previous/next pager, and home-page block; the rank values themselves never change. With note_tray_sidebar: "year" or "month", groups are chronological in that direction and items inside each group follow rank. Keep sidebar_position monotonic with date so grouped dates and reading order agree. See Note Trays for the complete setup.
Note
_category_.json still works but triggers a zfb build warning. Prefer frontmatter on index.mdx for new categories.
You can also set sortOrder in a bound sidebar configuration for manual sidebar configurations:
{
type: "category",
label: "Releases",
sortOrder: "desc",
items: [
{ type: "autogenerated" },
],
}
Tip
For changelog-style categories, combine sortOrder: "desc" with date-prefixed filenames (e.g., 2026-03-10-release.mdx) for automatic newest-first ordering without manually setting sidebar_position.
Ordering Recommendations
Set
sidebar_positionon every page for predictable orderingUse
sidebar_position: 0on category index pagesUse sequential numbers (1, 2, 3...) for pages within each category
Leave gaps between numbers (e.g., 10, 20, 30) if you expect to insert pages later
Use
category_sort_order: "desc"inindex.mdxfor newest-first categoriesFor a note tray, choose
note_tray_sidebar: "index","year", or"month"; year and month require dates on every item
Broader Tree
The controls below the filter change the visible navigation tree without changing the article, reading position, or table of contents. Broaden tree moves to the immediate editorial parent. At the highest available tree the whole scope toolbar (Broaden and Restore) is hidden; there is no disabled button or hint. A custom forest can be that highest tree.
Each category has a separate Show only this branch target button. Category links still open pages, and disclosure buttons still expand or collapse children. Restore current page’s local tree appears while a different scope is selected and returns to the current page’s configured roots and order. These controls also work in the mobile drawer without closing it; page links close it normally.
The filter stays in place when changing scope. A matching category keeps its subtree; a matching descendant keeps its ancestors. Branch expansion choices survive Broaden and branch focus. Restore returns to the configured baseline (including initialPathMode: "node"), clears the selected scope and the saved expand/collapse choices, keeps the filter text, and reveals the current page; a filter that excludes that page must be cleared to show it. The desktop sidebar and the mobile drawer update together. Trees remain recursive with no depth limit.
Scope is temporary session state. initialPath and initialPathMode define only the fresh/Restore baseline. Sections that share an editorial context (the same contextId for the same complete items tree, or the automatic tree) share saved state, so a broadened or focused scope the reader chose stays in effect across those pages until Restore. Following a link inside the selected tree keeps it; another branch widens to the nearest shared editorial ancestor. A saved scope whose view equals the current page's baseline is treated as the baseline, so no Restore button appears. Locale, version, or independent document-context changes return to the destination’s configured sidebar. Changed topology invalidates stored scopes. URLs and modified-click/new-tab link behavior stay native.
Explicit Editorial Context
Existing array configurations keep their initial behavior. To supply parents outside a local view, use the object form: items describes the complete editorial tree and initialPath selects the children of a category by zero-based indexes in the resolved, ordered tree. In this example the initial forest contains Background and Border; the first broader view is Surfaces & borders, followed by Utility reference. These groups need not appear in document URLs.
import type { SidebarsConfig } from "@takazudo/zudo-doc/sidebar-utils";
const sidebars: SidebarsConfig = {
utilities: {
contextId: "utility-reference",
items: [
{
type: "category",
label: "Utility reference",
items: [
{
type: "category",
label: "Surfaces & borders",
items: ["utilities/background", "utilities/border"],
},
],
},
],
initialPath: [0, 0],
},
};
export default sidebars;Bind this object through chromeBindings.sidebarsConfig as in the configuration example above. contextId is optional; use the same value for sections that share the same complete items tree but choose different initialPath values. Locale, version, and topology revision are added automatically. Invalid paths fail the build rather than inventing parents.
Selecting the Node Itself
initialPathMode controls what initialPath selects. The default, "children", shows the children of the selected category. With "node", the selected node itself is the baseline: its row (a page-backed section keeps its overview link) with its children. navigation.roots still holds the complete editorial tree, so Broaden goes to the target's actual enclosing parent and then on up, without a duplicate or invented parent row.
import type { SidebarsConfig } from "@takazudo/zudo-doc/sidebar-utils";
const sidebars: SidebarsConfig = {
wind: {
items: [{ type: "autogenerated", dirName: "wind" }],
// Utility reference → Surfaces & borders
initialPath: [0, 0],
initialPathMode: "node",
},
};
export default sidebars;Take an autogenerated tree Wind → Utility reference → Surfaces & borders → Background color, with initialPath pointing at Surfaces & borders. On Background color, node mode first shows Surfaces & borders (linked to its overview) with its pages; Broaden then shows Utility reference, then Wind. Children mode first shows only the pages of Surfaces & borders, and the first Broaden adds the Surfaces & borders row.
A missing or empty initialPath keeps the whole resolved forest in either mode. A valid path ending at a leaf page is allowed in node mode and shows that page alone. Invalid indexes and any other initialPathMode value fail the build. Both modes apply identically to the desktop sidebar and the mobile drawer.
An object definition with empty items (items: []) uses the automatic category-filtered tree, exactly like an omitted entry, and then applies initialPath and initialPathMode to it. Both buildSidebarForSection and buildSidebarNavigation agree on the result, and invalid paths or modes fail the same way. A missing entry or an empty array keeps the plain automatic fallback.
An autogenerated entry retains the provenance of its omitted directory root. Authored groupings remain authoritative, and duplicate labels or links keep separate occurrence identities. A custom forest without a shared parent stops at that forest. Header dropdown grouping is not document ancestry.
Custom integrations can call buildSidebarNavigation from @takazudo/ and pass its nodes and navigation to SidebarTree or SidebarToggle. buildSidebarForSection and nodes-only island props remain supported; nodes-only integrations keep the existing tree without scope controls.
Client Navigation Data
The native header and sidebar factories copy only the fields needed by their sidebar islands. Page descriptions remain in server navigation data for category rendering, but are omitted recursively from both visible nodes and the broader tree sent to the browser. Mobile sidebarNodes arrays and callback results are copied without remapping URLs; their object identity is not retained.
Custom hosts can call toSidebarClientData(nodes, navigation) from @takazudo/ before constructing a SidebarTree or SidebarToggle island, then pass the returned object to both SSR and hydration. The helper validates input before selecting fields and preserves occurrence IDs, topology, expansion defaults and the existing navigation context ID. Descriptions can still affect that context ID; this is payload minimization, not a privacy boundary or a change to scope persistence.