Relevant source files
The following files were used as context for generating this wiki page:
- packages/core/src/source/page-tree/builder.ts
- packages/radix-ui/src/components/sidebar/page-tree.tsx
- packages/base-ui/src/components/sidebar/page-tree.tsx
- packages/radix-ui/src/layouts/notebook/slots/sidebar.tsx
- packages/base-ui/src/layouts/notebook/slots/sidebar.tsx
- packages/asyncapi/src/server/index.tsx
- packages/base-ui/src/components/sidebar/tabs/index.tsx
- packages/radix-ui/src/components/sidebar/tabs/index.tsx
- packages/core/src/source/loader.ts
- packages/core/src/source/plugins/status-badges.tsx
- packages/radix-ui/src/utils/use-footer-items.ts
- packages/base-ui/src/layouts/flux/slots/sidebar.tsx
- packages/radix-ui/src/layouts/flux/slots/sidebar.tsx
- packages/core/src/source/llms.ts
- packages/cli/src/commands/file-tree.ts
- packages/base-ui/src/utils/use-footer-items.ts
- packages/radix-ui/src/contexts/tree.tsx
- packages/core/src/source/client/index.tsx
- packages/openapi/src/server/index.tsx
- packages/base-ui/src/contexts/tree.tsx
- packages/core/src/source/page-tree/transformer-fallback.ts
- packages/core/src/page-tree/utils.ts
- packages/openapi/src/generate-file.ts
- packages/core/src/breadcrumb.tsx
- packages/epub/src/toc-builder.ts
- packages/openapi/src/utils/pages/builder.ts
- packages/asyncapi/src/utils/pages/preset-auto.ts
- packages/base-ui/src/components/sidebar/link-item.tsx
- packages/radix-ui/src/components/sidebar/link-item.tsx
- packages/radix-ui/src/layouts/shared/index.tsx
Overview
Page tree generation constructs and manages the hierarchical navigation models that organize documentation content across the Fumadocs framework ecosystem. By parsing content files, resolving directory structures, and processing metadata, the builder turns raw file storage into structured node primitives including roots, folders, items, and separators.
Sources: packages/core/src/source/page-tree/builder.ts:72-87, packages/core/src/source/loader.ts:1-15
Page Tree Data Structures
Node Types and Primitives
The core page tree architecture represents documentation structures through a set of primitive node schemas and symbols. Internal builder structures attach metadata such as SymbolUnfinished, SymbolName, and SymbolOwner to track ownership priority and resolve references when multiple folders claim the same node.
Sources: packages/core/src/source/page-tree/builder.ts:74-82
The builder interface exposes path resolution and root generation capabilities through the PageTreeBuilder contract.
export interface PageTreeBuilder {
resolveFlattenPath(name: string, format: string): string;
root(id?: string, path?: string): PageTree.Root;
}Sources: packages/core/src/source/page-tree/builder.ts:84-87
Tree traversal utilities operate on nodes and roots, flattening hierarchical structures into ordered arrays of page items, locating neighboring nodes for pagination, and discovering parent containers via depth-first searches.
Sources: packages/core/src/page-tree/utils.ts:6-19, packages/core/src/page-tree/utils.ts:24-33, packages/core/src/page-tree/utils.ts:105-119
Hierarchical Tree Builder
File Scanning and Path Resolution
The builder initializes by scanning storage instances via createPageTreeBuilder, mapping every file and its content format into a flattened path lookup table. The excludePrefix constant (!) identifies excluded items during tree construction, while internal symbols track state: SymbolUnfinished marks incomplete nodes, SymbolName stores display identifiers, and SymbolOwner records ownership objects consisting of an owner path and priority.
const excludePrefix = '!';
const SymbolUnfinished = Symbol('unfinished');
const SymbolName = Symbol('name');
const SymbolOwner = Symbol('owner');
interface Attached {
[SymbolName]?: string;
[SymbolUnfinished]?: boolean;
[SymbolOwner]?: { owner: string; priority: number };
}Sources: packages/core/src/source/page-tree/builder.ts:72-82
Content storages are iterated to register file paths by stripping their extensions and concatenating format extensions into flattenPathToFullPath mapping entries. The resolveFlattenPath method retrieves the full file path from a given name and format or falls back to the original name.
for (const file of storage.getFiles()) {
const content = storage.read(file)!;
const flattenPath = file.substring(0, file.length - extname(file).length);
flattenPathToFullPath.set(flattenPath + '.' + content.format, file);
}
function resolveFlattenPath(name: string, format: string) {
return flattenPathToFullPath.get(name + '.' + format) ?? name;
}Sources: packages/core/src/source/page-tree/builder.ts:140-149
Ownership Tracking and Transfer
When multiple folders reference the same node, ownership tracking determines container assignment based on priority values. The own function checks if a node is unfinished, evaluates existing ownership, updates matching ownership priorities, or revokes ownership from lower-priority folders by splicing nodes out of their children lists and clearing folder index references.
function own(ownerPath: string, node: PageTree.Node & Attached, priority: number): boolean {
if (node[SymbolUnfinished]) return false;
const existing = node[SymbolOwner];
if (!existing) {
node[SymbolOwner] = { owner: ownerPath, priority };
return true;
}
if (existing.owner === ownerPath) {
existing.priority = Math.max(existing.priority, priority);
return true;
}
if (existing.priority >= priority) return false;
// return ownership
const folder = pathToNode.get(existing.owner);
if (folder && folder.type === 'folder') {
if (folder.index === node) {
delete folder.index;
} else {
const idx = folder.children.indexOf(node);
if (idx !== -1) folder.children.splice(idx, 1);
}
}
existing.owner = ownerPath;
existing.priority = priority;
return true;
}
function transferOwner(ownerPath: string, node: PageTree.Node & Attached) {
const existing = node[SymbolOwner];
if (existing) existing.owner = ownerPath;
}Sources: packages/core/src/source/page-tree/builder.ts:158-190
Note
Lower priority owners attempting to claim a node already owned by a higher priority parent will have their claim rejected, returning false without modifying existing ownership boundaries.
Sources: packages/core/src/source/page-tree/builder.ts:169-169
Fallback Transformation
The fallback mechanism implemented in transformerFallback captures unreferenced files and directories that were not visited during standard tree building. It isolates remaining storage items and generates a secondary fallback page tree prefixed with fallback.
export function transformerFallback(): PageTreeTransformer {
const addedFiles = new Set<string>();
function shouldIgnore(context: PageTreeBuilderContext) {
return context.custom?._fallback === true;
}
return {
root(root) {
if (shouldIgnore(this)) return root;
const isolatedStorage = new FileSystem() as ContentStorage;
if (addedFiles.size === this.storage.files.size) return root;
for (const file of this.storage.getFiles()) {
if (addedFiles.has(file)) continue;
isolatedStorage.write(file, this.storage.read(file)!);
}
root.fallback = createPageTreeBuilder(isolatedStorage, {
...this.options,
idPrefix: this.options.idPrefix ? `fallback:${this.options.idPrefix}` : 'fallback',
generateFallback: false,
context: { ...this.custom, _fallback: true },
}).root();
addedFiles.clear();
return root;
},
file(node, file) {
if (shouldIgnore(this)) return node;
if (file) addedFiles.add(file);
return node;
},
folder(node, _dir, metaPath) {
if (shouldIgnore(this)) return node;
if (metaPath) addedFiles.add(metaPath);
return node;
},
};
}Sources: packages/core/src/source/page-tree/transformer-fallback.ts:5-45
Tree Navigation and Traversal Utilities
Depth-First Tree Visitation and Mutation
The visit function executes a depth-first search across a page tree or node collection, invoking a visitor callback for each encountered node. The visitor callback can return 'skip' to bypass children of the current node, 'break' to terminate the search entirely via a thrown VisitBreak symbol caught internally, or a modified node object to perform in-place tree transformations.
export function visit<Root extends PageTree.Node | PageTree.Root>(
root: Root,
visitor: <T extends PageTree.Node | PageTree.Root>(
node: T,
parent?: PageTree.Root | PageTree.Folder,
) => 'skip' | 'break' | T | void,
): Root {
function onNode<T extends PageTree.Node | PageTree.Root>(
node: T,
parent?: PageTree.Root | PageTree.Folder,
): T {
const result = visitor(node, parent);
switch (result) {
case 'skip':
return node;
case 'break':
throw VisitBreak;
default:
if (result) node = result;
}
if ('index' in node && node.index) {
node.index = onNode(node.index, node);
}
if ('fallback' in node && node.fallback) {
node.fallback = onNode(node.fallback, node);
}
if ('children' in node) {
for (let i = 0; i < node.children.length; i++) {
node.children[i] = onNode(node.children[i], node);
}
}
return node;
}
try {
return onNode(root);
} catch (e) {
if (e === VisitBreak) return root;
throw e;
}
}Sources: packages/core/src/page-tree/utils.ts:176-220
Warning
Returning 'break' from a visitor callback throws an internal VisitBreak symbol that is intercepted by visit to cleanly return the unmodified root reference, avoiding unhandled exceptions during traversal aborts.
Sources: packages/core/src/page-tree/utils.ts:168-175, packages/core/src/page-tree/utils.ts:214-219
Path Lookup and Neighbor Resolution
Path lookup and list flattening utilities transform hierarchical tree structures into linear arrays or specific ancestor trails. The flattenTree function iterates over nodes, extracting folder indices and nested children recursively into a flat list of page items.
export function flattenTree(nodes: PageTree.Node[]): PageTree.Item[] {
const out: PageTree.Item[] = [];
for (const node of nodes) {
if (node.type === 'folder') {
if (node.index) out.push(node.index);
out.push(...flattenTree(node.children));
} else if (node.type === 'page') {
out.push(node);
}
}
return out;
}Sources: packages/core/src/page-tree/utils.ts:6-19
The findPath function searches for a node matching a predicate, optionally including separators in the returned path array. Breadcrumb generation leverages searchPath to resolve the current URL against normalized tree paths.
export function findPath(
nodes: PageTree.Node[],
matcher: (node: PageTree.Node) => boolean,
options: {
includeSeparator?: boolean;
} = {},
): PageTree.Node[] | null {
const { includeSeparator = true } = options;
function run(nodes: PageTree.Node[]): PageTree.Node[] | undefined {
let separator: PageTree.Separator | undefined;
for (const node of nodes) {
if (matcher(node)) {
const items: PageTree.Node[] = [];
if (separator) items.push(separator);
items.push(node);
return items;
}
if (node.type === 'separator' && includeSeparator) {
separator = node;
continue;
}
if (node.type === 'folder') {
const items = node.index && matcher(node.index) ? [node.index] : run(node.children);
if (items) {
items.unshift(node);
if (separator) items.unshift(separator);
return items;
}
}
}
}
return run(nodes) ?? null;
}Sources: packages/core/src/page-tree/utils.ts:126-166
| Utility Function | Parameters | Return Type | Description |
|---|---|---|---|
flattenTree | nodes: PageTree.Node[] | PageTree.Item[] | Flattens folders, index nodes, and pages into a single sequential list. |
findNeighbour | tree: PageTree.Root, url: string, options?: { separateRoot?: boolean } | { previous?: PageTree.Item, next?: PageTree.Item } | Resolves adjacent previous and next page items for pagination controls. |
getPageTreeRoots | pageTree: PageTree.Root | PageTree.Folder | (PageTree.Root | PageTree.Folder)[] | Collects all root nodes and nested root folders. |
getPageTreePeers | treeOrTrees: PageTree.Root | Record<string, PageTree.Root>, url: string | PageTree.Item[] | Retrieves peer page nodes residing under the same parent container. |
findSiblings | treeOrTrees: PageTree.Root | Record<string, PageTree.Root>, url: string | PageTree.Node[] | Retrieves all sibling tree nodes under the same parent. |
findParent | from: PageTree.Root | PageTree.Folder, url: string | PageTree.Root | PageTree.Folder | undefined | Locates the parent container of a page matched by its URL. |
findPath | nodes: PageTree.Node[], matcher: (node: PageTree.Node) => boolean, options?: { includeSeparator?: boolean } | PageTree.Node[] | null | Finds the array of nodes forming the path to a matched node. |
Sources: packages/core/src/page-tree/utils.ts:6-166
Status Badges and Tree Transformation
Status Badges Plugin Architecture
The statusBadgesPlugin function returns a loader plugin named fumadocs:status-badges that decorates navigation items with status indicators by inspecting document frontmatter. When processing a file node, the transformer checks whether the file format is 'page' and whether a string-typed status property exists within file.data.
export function statusBadgesPlugin(
options: {
renderBadge?: (status: string) => ReactNode;
} = {},
): LoaderPlugin {
const { renderBadge = (status) => <span data-status={status}>{status}</span> } = options;
return {
name: 'fumadocs:status-badges',
transformPageTree: {
file(node, filePath) {
if (!filePath) return node;
const file = this.storage.read(filePath);
if (
file?.format === 'page' &&
'status' in file.data &&
typeof file.data.status === 'string'
) {
const status = file.data.status;
node.name = (
<>
{node.name}
{renderBadge(status)}
</>
);
(node as Item).status = status;
}
return node;
},
},
};Sources: packages/core/src/source/plugins/status-badges.tsx:50-83
If a valid status string is located, the plugin modifies node.name by appending a rendered badge element alongside the original node name, and assigns the raw status string directly to (node as Item).status. Consumers can customize badge rendering by passing a renderBadge callback option; if omitted, the plugin defaults to rendering a <span> element equipped with a data-status attribute.
const { renderBadge = (status) => <span data-status={status}>{status}</span> } = options;
return {
name: 'fumadocs:status-badges',
transformPageTree: {
file(node, filePath) {
if (!filePath) return node;
const file = this.storage.read(filePath);
if (
file?.format === 'page' &&
'status' in file.data &&
typeof file.data.status === 'string'
) {
const status = file.data.status;
node.name = (
<>
{node.name}
{renderBadge(status)}
</>
);
(node as Item).status = status;
}Sources: packages/core/src/source/plugins/status-badges.tsx:55-78
Note
The statusBadgesPlugin strictly requires filePath to be defined on the node and relies on this.storage.read(filePath) to access document frontmatter during tree transformation.
Sources: packages/core/src/source/plugins/status-badges.tsx:60-63
Fallback Tree Transformation
The transformerFallback function constructs a secondary fallback page tree builder by intercepting root, file, and folder transformation hooks. It tracks processed files in an internal addedFiles set and inspects the builder context via shouldIgnore to verify whether context.custom?._fallback is enabled, preventing recursive fallback generation loops.
export function transformerFallback(): PageTreeTransformer {
const addedFiles = new Set<string>();
function shouldIgnore(context: PageTreeBuilderContext) {
return context.custom?._fallback === true;
}
return {
root(root) {
if (shouldIgnore(this)) return root;
const isolatedStorage = new FileSystem() as ContentStorage;
if (addedFiles.size === this.storage.files.size) return root;
for (const file of this.storage.getFiles()) {
if (addedFiles.has(file)) continue;
isolatedStorage.write(file, this.storage.read(file)!);
}
root.fallback = createPageTreeBuilder(isolatedStorage, {
...this.options,
idPrefix: this.options.idPrefix ? `fallback:${this.options.idPrefix}` : 'fallback',
generateFallback: false,
context: { ...this.custom, _fallback: true },
}).root();
addedFiles.clear();
return root;
},
file(node, file) {
if (shouldIgnore(this)) return node;
if (file) addedFiles.add(file);
return node;
},
folder(node, _dir, metaPath) {
if (shouldIgnore(this)) return node;
if (metaPath) addedFiles.add(metaPath);
return node;
},
};Sources: packages/core/src/source/page-tree/transformer-fallback.ts:5-45
When the root transformer executes, it initializes an isolated file system storage instance, copies unindexed files that are absent from addedFiles, and instantiates a nested page tree builder with a modified idPrefix and a restricted context where _fallback is set to true.
root(root) {
if (shouldIgnore(this)) return root;
const isolatedStorage = new FileSystem() as ContentStorage;
if (addedFiles.size === this.storage.files.size) return root;
for (const file of this.storage.getFiles()) {
if (addedFiles.has(file)) continue;
isolatedStorage.write(file, this.storage.read(file)!);
}
root.fallback = createPageTreeBuilder(isolatedStorage, {
...this.options,
idPrefix: this.options.idPrefix ? `fallback:${this.options.idPrefix}` : 'fallback',
generateFallback: false,
context: { ...this.custom, _fallback: true },
}).root();
Sources: packages/core/src/source/page-tree/transformer-fallback.ts:12-29
Warning
The fallback transformer clears addedFiles immediately after constructing the fallback tree root, ensuring subsequent transformation passes evaluate file additions correctly.
Sources: packages/core/src/source/page-tree/transformer-fallback.ts:30-31
Client Serialization and State Contexts
Serialization and Hydration
Page trees generated by server-side loaders are serialized into a standard format before transmission across client-server boundaries. The SerializedPageTree interface wraps data using the $fumadocs_loader discriminator set to 'page-tree'.
export interface SerializedPageTree {
$fumadocs_loader: 'page-tree';
data: object;
}Sources: packages/core/src/source/client/index.tsx:5-8
The useFumadocsLoader hook processes serialized loader data, identifying serialized page trees and passing them to deserializePageTree for hydration.
export function useFumadocsLoader<V>(serialized: V): Serialized<V> {
return useMemo(() => {
const out: Record<string, unknown> = {};
for (const k in serialized) {
const v = serialized[k];
if (isSerializedPageTree(v)) {
out[k] = deserializePageTree(v);
} else {
out[k] = v;
}
}
return out as Serialized<V>;
}, [serialized]);
}Sources: packages/core/src/source/client/index.tsx:47-60
During deserialization, visit traverses the page tree nodes to parse and inject HTML strings for icon and name properties via dangerouslySetInnerHTML.
export function deserializePageTree(serialized: SerializedPageTree): PageTree.Root {
const root = serialized.data as PageTree.Root;
visit(root, (item) => {
if ('icon' in item && typeof item.icon === 'string') {
item.icon = (
<span
dangerouslySetInnerHTML={{
__html: item.icon,
}}
/>
);
}
if (typeof item.name === 'string') {
item.name = (
<span
className="fd-page-tree-item-name"
dangerouslySetInnerHTML={{
__html: item.name,
}}
/>
);
}
});
return root;
}Sources: packages/core/src/source/client/index.tsx:14-39
Note
deserializePageTree inspects every visited node and converts string-based icons and names into React element structures containing dangerouslySetInnerHTML wrappers.
Sources: packages/core/src/source/client/index.tsx:16-35
Tree Contexts and Path Tracking
The TreeContextProvider component manages active tree state, path breadcrumbs, and unique root identifiers across client navigation. It computes the active breadcrumb path by searching the tree and its fallback children against the current pathname.
Sources: packages/radix-ui/src/contexts/tree.tsx:17-47
| Context/Hook | Return Type | Purpose |
|---|---|---|
TreeContext | TreeContextType | Holds the active root node (root) and the complete page tree (full). |
PathContext | PageTree.Node[] | Tracks the hierarchical node path leading to the current active pathname. |
useTreePath() | PageTree.Node[] | Retrieves the current path node array from PathContext. |
useTreeContext() | TreeContextType | Retrieves the active tree context, throwing an error if uninitialized. |
Sources: packages/radix-ui/src/contexts/tree.tsx:9-12, packages/radix-ui/src/contexts/tree.tsx:15-15, packages/radix-ui/src/contexts/tree.tsx:49-57
Warning
useTreeContext throws an explicit error if invoked outside of a component wrapped under <DocsLayout />.
Sources: packages/radix-ui/src/contexts/tree.tsx:53-57
Sidebar Node Rendering and Navigation
Recursive Folder Hierarchies and Active States
The createPageTreeRenderer function generates React component trees from page tree structures by recursively traversing nodes via renderList and PageTreeNode. It evaluates each node type—separator, folder, or page item—and dispatches rendering to either custom user-supplied components or default UI primitives.
export function createPageTreeRenderer({
SidebarFolder,
SidebarFolderContent,
SidebarFolderLink,
SidebarFolderTrigger,
SidebarSeparator,
SidebarItem,
}: InternalComponents) {
function renderList(nodes: PageTree.Node[]) {
return nodes.map((node, i) => <PageTreeNode key={i} node={node} />);
}
function PageTreeNode({ node }: { node: PageTree.Node }) {
const { Separator, Item, Folder, pathname } = use(RendererContext)!;
if (node.type === 'separator') {
if (Separator) return <Separator item={node} />;
return (
<SidebarSeparator>
{node.icon}
{node.name}
</SidebarSeparator>
);
}
if (node.type === 'folder') {
// eslint-disable-next-line react-hooks/rules-of-hooks -- assume node type unchanged
const path = useTreePath();
if (Folder) return <Folder item={node}>{renderList(node.children)}</Folder>;
return (
<SidebarFolder
collapsible={node.collapsible}
active={path.includes(node)}
defaultOpen={node.defaultOpen}
>
{node.index ? (
<SidebarFolderLink
href={node.index.url}
active={isActive(node.index.url, pathname)}
external={node.index.external}
>
{node.icon}
{node.name}
</SidebarFolderLink>
) : (
<SidebarFolderTrigger>
{node.icon}
{node.name}
</SidebarFolderTrigger>
)}
<SidebarFolderContent>{renderList(node.children)}</SidebarFolderContent>
</SidebarFolder>
);
}
if (Item) return <Item item={node} />;
return (
<SidebarItem
href={node.url}
external={node.external}
active={isActive(node.url, pathname)}
icon={node.icon}
>
{node.name}
</SidebarItem>
);
}Sources: packages/radix-ui/src/components/sidebar/page-tree.tsx:31-98
Active states for individual items and index links are determined by comparing their target URLs against the current router pathname using the isActive utility. For folders, the useTreePath hook checks whether the folder is included in the current branch path to set expansion and highlight states.
if (node.type === 'folder') {
// eslint-disable-next-line react-hooks/rules-of-hooks -- assume node type unchanged
const path = useTreePath();
if (Folder) return <Folder item={node}>{renderList(node.children)}</Folder>;
return (
<SidebarFolder
collapsible={node.collapsible}
active={path.includes(node)}
defaultOpen={node.defaultOpen}
>
{node.index ? (
<SidebarFolderLink
href={node.index.url}
active={isActive(node.index.url, pathname)}
external={node.index.external}
>
{node.icon}
{node.name}
</SidebarFolderLink>
) : (
<SidebarFolderTrigger>
{node.icon}
{node.name}
</SidebarFolderTrigger>
)}
<SidebarFolderContent>{renderList(node.children)}</SidebarFolderContent>
</SidebarFolder>
);
}
if (Item) return <Item item={node} />;
return (
<SidebarItem
href={node.url}
external={node.external}
active={isActive(node.url, pathname)}
icon={node.icon}
>
{node.name}
</SidebarItem>
);Sources: packages/radix-ui/src/components/sidebar/page-tree.tsx:56-97
Note
RendererContext caches the active pathname alongside custom component overrides using useMemo, ensuring minimal re-renders during client-side navigation.
Sources: packages/radix-ui/src/components/sidebar/page-tree.tsx:14-19, packages/radix-ui/src/components/sidebar/page-tree.tsx:109-114
Tab Partitioning and Folder Scanning
The getSidebarTabs utility extracts layout tabs from root page trees by scanning folders recursively. It identifies root-level sections and builds a collection of valid navigation endpoints.
export function getSidebarTabs(
tree: PageTree.Root,
{ transform = defaultTransform }: GetSidebarTabsOptions = {},
): SidebarTab[] {
const results: SidebarTab[] = [];
function scanOptions(node: PageTree.Root | PageTree.Folder, unlisted?: boolean) {
if ('root' in node && node.root) {
const urls = getFolderUrls(node);
if (urls.size > 0) {
const option: SidebarTab = {
url: urls.values().next().value ?? '',
title: node.name,
icon: node.icon,
unlisted,
description: node.description,
urls,
};
const mapped = transform ? transform(option, node) : option;
if (mapped) results.push(mapped);
}
}
for (const child of node.children) {
if (child.type === 'folder') scanOptions(child, unlisted);
}
}
scanOptions(tree);
if (tree.fallback) scanOptions(tree.fallback, true);
return results;
}Sources: packages/base-ui/src/components/sidebar/tabs/index.tsx:23-57
| Helper Function | Input Parameters | Return Type | Description |
|---|---|---|---|
getSidebarTabs | tree: PageTree.Root, options?: GetSidebarTabsOptions | SidebarTab[] | Scans root nodes and fallback trees to construct partition tabs. |
getFolderUrls | folder: PageTree.Folder, output?: Set<string> | Set<string> | Collects all non-external page URLs and folder index links within a branch. |
Sources: packages/base-ui/src/components/sidebar/tabs/index.tsx:23-26, packages/base-ui/src/components/sidebar/tabs/index.tsx:59-67
Warning
If a scanned folder contains no resolvable child pages or index URLs, it is omitted from the resulting sidebar tabs array.
Sources: packages/base-ui/src/components/sidebar/tabs/index.tsx:30-45
Sequential Footer Item Navigation
The useFooterItems hook produces a flattened, linear array of page tree items suitable for generating footer pagination and sequential next/previous navigation links.
export function useFooterItems(): PageTree.Item[] {
const { root } = useTreeContext();
const cached = footerCache.get(root);
if (cached) return cached;
const list: PageTree.Item[] = [];
function onNode(node: PageTree.Node) {
if (node.type === 'folder') {
if (node.index) onNode(node.index);
for (const child of node.children) onNode(child);
} else if (node.type === 'page' && !node.external) {
list.push(node);
}
}
for (const child of root.children) onNode(child);
footerCache.set(root, list);
return list;Sources: packages/radix-ui/src/utils/use-footer-items.ts:10-27
During traversal, onNode processes folders by recursively inspecting folder index nodes and child elements, while filtering out external links and non-page entries before pushing items onto the linear list.
function onNode(node: PageTree.Node) {
if (node.type === 'folder') {
if (node.index) onNode(node.index);
for (const child of node.children) onNode(child);
} else if (node.type === 'page' && !node.external) {
list.push(node);
}
}Sources: packages/radix-ui/src/utils/use-footer-items.ts:16-23
Tip
useFooterItems caches generated item lists per root reference using a WeakMap, avoiding redundant tree traversals across repeated hook calls.
Sources: packages/radix-ui/src/utils/use-footer-items.ts:5-5, packages/radix-ui/src/utils/use-footer-items.ts:12-13, packages/radix-ui/src/utils/use-footer-items.ts:26-26
Related
On This Page
OverviewPage Tree Data StructuresNode Types and PrimitivesHierarchical Tree BuilderFile Scanning and Path ResolutionOwnership Tracking and TransferFallback TransformationTree Navigation and Traversal UtilitiesDepth-First Tree Visitation and MutationPath Lookup and Neighbor ResolutionStatus Badges and Tree TransformationStatus Badges Plugin ArchitectureFallback Tree TransformationClient Serialization and State ContextsSerialization and HydrationTree Contexts and Path TrackingSidebar Node Rendering and NavigationRecursive Folder Hierarchies and Active StatesTab Partitioning and Folder ScanningSequential Footer Item NavigationRelated