Home /

docs

Sidebar

The sidebar is auto-generated from your file structure. Customize its appearance through config.

Custom Title

The sidebar title supports strings or React components:

docs.config.tsx
nav: {
  title: (
    <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
      <Rocket size={14} />
      <span>My Docs</span>
    </div>
  ),
  url: "/docs",
},

Icons

Add icons to sidebar items via frontmatter:

page.mdx
---
title: "Getting Started"
icon: "rocket"
---

Register icons in your config:

docs.config.tsx
icons: {
  rocket: <Rocket size={16} />,
  book: <BookOpen size={16} />,
},

Collapsible Groups

Nested directories automatically become collapsible groups. A directory with its own page.mdx becomes a folder with an index page:

app/docs/
  plugins/
    page.mdx            # Folder index (clickable)
    organizations/
      page.mdx           # Child page
    two-factor/
      page.mdx           # Child page

Flat Mode

Render all sidebar items without collapsible sections (Mintlify-style):

docs.config.tsx
sidebar: { flat: true },

Add custom content above and below the navigation items using banner and footer:

docs.config.tsx
sidebar: {
  banner: (
    <div style={{
      padding: "12px 16px",
      borderBottom: "1px solid var(--color-fd-border)",
      fontSize: "13px",
    }}>
      <strong>v2.0 is here</strong>
      <p style={{ margin: 0 }}>Check out the new features.</p>
    </div>
  ),
  footer: (
    <div style={{
      padding: "12px 16px",
      borderTop: "1px solid var(--color-fd-border)",
      fontSize: "12px",
      color: "var(--color-fd-muted-foreground)",
    }}>
      Built with @farming-labs/docs
    </div>
  ),
},

The banner renders above the navigation items and footer renders below them, both inheriting the theme's sidebar styling automatically.

Next.js and TanStack Start can pass React nodes directly in docs.config.tsx. For non-React frameworks, use named slots:

Pass sidebarHeader and sidebarFooter snippets:

+layout.svelte
<DocsLayout tree={data.tree} config={data.config}>
  {#snippet sidebarHeader()}
    <div class="sidebar-promo">v2.0 is here</div>
  {/snippet}
  {#snippet sidebarFooter()}
    <div class="sidebar-credits">Built with docs</div>
  {/snippet}
  {@render children()}
</DocsLayout>

Use sidebar-header and sidebar-footer named slots:

layouts/docs.astro
<DocsLayout tree={tree} config={config}>
  <div slot="sidebar-header" class="sidebar-promo">
    v2.0 is here
  </div>
  <div slot="sidebar-footer" class="sidebar-credits">
    Built with docs
  </div>
  <slot />
</DocsLayout>

Use #sidebar-header and #sidebar-footer slots:

layouts/docs.vue
<DocsLayout :tree="tree" :config="config">
  <template #sidebar-header>
    <div class="sidebar-promo">v2.0 is here</div>
  </template>
  <template #sidebar-footer>
    <div class="sidebar-credits">Built with docs</div>
  </template>
  <slot />
</DocsLayout>

Custom Sidebar Component

Replace the entire sidebar navigation with your own component. The component receives the full page tree with all parent-child relationships.

Pass a render function via sidebar.component in your config:

docs.config.tsx
import type { SidebarComponentProps } from "@farming-labs/docs";

sidebar: {
  component: ({ tree, collapsible, flat }: SidebarComponentProps) => (
    <MySidebar tree={tree} />
  ),
},

Your component receives the full tree structure:

components/MySidebar.tsx
"use client";
import { usePathname } from "next/navigation";
import type { SidebarTree, SidebarNode } from "@farming-labs/docs";

export function MySidebar({ tree }: { tree: SidebarTree }) {
  const pathname = usePathname();

  function renderNode(node: SidebarNode) {
    if (node.type === "page") {
      return (
        <a
          href={node.url}
          className={pathname === node.url ? "active" : ""}
        >
          {node.name}
        </a>
      );
    }

    // node.type === "folder"
    return (
      <details open>
        <summary>{node.name}</summary>
        <div>
          {node.index && (
            <a href={node.index.url}>{node.index.name}</a>
          )}
          {node.children.map((child) => renderNode(child))}
        </div>
      </details>
    );
  }

  return (
    <nav>
      {tree.children.map((node) => renderNode(node))}
    </nav>
  );
}

Pass a render function via sidebar.component in your config:

docs.config.tsx
import type { SidebarComponentProps } from "@farming-labs/docs";

sidebar: {
  component: ({ tree, collapsible, flat }: SidebarComponentProps) => (
    <MySidebar tree={tree} />
  ),
},

Your component can read the current route with TanStack Router:

src/components/MySidebar.tsx
import { useRouterState } from "@tanstack/react-router";
import type { SidebarTree, SidebarNode } from "@farming-labs/docs";

export function MySidebar({ tree }: { tree: SidebarTree }) {
  const pathname = useRouterState({
    select: (state) => state.location.pathname,
  });

  function renderNode(node: SidebarNode) {
    if (node.type === "page") {
      return (
        <a
          href={node.url}
          className={pathname === node.url ? "active" : ""}
        >
          {node.name}
        </a>
      );
    }

    return (
      <details open>
        <summary>{node.name}</summary>
        <div>
          {node.index && <a href={node.index.url}>{node.index.name}</a>}
          {node.children.map((child) => renderNode(child))}
        </div>
      </details>
    );
  }

  return <nav>{tree.children.map((node) => renderNode(node))}</nav>;
}

Use the sidebar snippet on <DocsLayout>. It receives tree and isActive:

+layout.svelte
<script>
  import DocsLayout from "@farming-labs/svelte-theme/DocsLayout.svelte";
  let { data, children } = $props();
</script>

<DocsLayout tree={data.tree} config={data.config}>
  {#snippet sidebar({ tree, isActive })}
    <nav>
      {#each tree.children as node}
        {#if node.type === "page"}
          <a
            href={node.url}
            class:active={isActive(node.url)}
          >
            {node.name}
          </a>
        {:else}
          <details open>
            <summary>{node.name}</summary>
            {#each node.children as child}
              {#if child.type === "page"}
                <a href={child.url}>{child.name}</a>
              {/if}
            {/each}
          </details>
        {/if}
      {/each}
    </nav>
  {/snippet}
  {@render children()}
</DocsLayout>

Use the sidebar named slot:

layouts/docs.astro
---
import DocsLayout from "@farming-labs/astro-theme/DocsLayout.astro";
import MySidebar from "../components/MySidebar.astro";
const { tree, config } = Astro.props;
---

<DocsLayout tree={tree} config={config}>
  <MySidebar slot="sidebar" tree={tree} />
  <slot />
</DocsLayout>

Use the #sidebar scoped slot:

layouts/docs.vue
<template>
  <DocsLayout :tree="tree" :config="config">
    <template #sidebar="{ tree, isActive }">
      <nav>
        <template v-for="node in tree.children" :key="node.name">
          <a
            v-if="node.type === 'page'"
            :href="node.url"
            :class="{ active: isActive(node.url) }"
          >
            {{ node.name }}
          </a>
          <details v-else open>
            <summary>{{ node.name }}</summary>
            <a
              v-for="child in node.children"
              v-if="child.type === 'page'"
              :key="child.url"
              :href="child.url"
            >
              {{ child.name }}
            </a>
          </details>
        </template>
      </nav>
    </template>
    <slot />
  </DocsLayout>
</template>

Tree Structure

The tree prop has this shape:

tree-structure.ts
interface SidebarTree {
  name: string;
  children: SidebarNode[];
}

type SidebarNode = SidebarPageNode | SidebarFolderNode;

interface SidebarPageNode {
  type: "page";
  name: string;
  url: string;
  icon?: unknown;
}

interface SidebarFolderNode {
  type: "folder";
  name: string;
  icon?: unknown;
  index?: SidebarPageNode;    // folder's own landing page
  children: SidebarNode[];    // recursive children
  collapsible?: boolean;
  defaultOpen?: boolean;
}

All types are exported from @farming-labs/docs for full type safety.

sidebar-style.tsx
theme: pixelBorder({
  ui: {
    sidebar: { style: "floating" },
  },
}),

Enable breadcrumb navigation:

docs.config.tsx
breadcrumb: { enabled: true },

Shows Parent / Current Page with the parent being clickable.