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 page
    organizations/
      page.mdx           # Child page
    two-factor/
      page.mdx           # Child page

Use sidebar.folderIndexBehavior when you want to control whether the parent row navigates, only expands/collapses, or stays as a plain label:

docs.config.tsx
sidebar: {
  folderIndexBehavior: "toggle",
},

With folderIndexBehavior: "toggle", the folder landing page is rendered as the first child item inside the group, and clicking the parent row no longer changes the URL. Set folderIndexBehavior: "link" if you want the parent row itself to navigate.

If you want to remove the folder landing-page link entirely and only show the child pages, use folderIndexBehavior: "hidden":

app/docs/sending/page.mdx
---
title: "Sending Messages"
sidebar:
  folderIndexBehavior: "hidden"
---

With folderIndexBehavior: "hidden", the framework omits the landing-page link from the sidebar tree and treats the folder like a label-only parent. In the default flat sidebar, that gives you a plain list of child links underneath. If someone manually opens the parent route, the framework redirects it to the first visible child. Hidden folder landing pages are also left out of the machine-readable markdown/search surfaces so they do not keep behaving like standalone docs pages.

If you only want that behavior in selected sections, use sidebar.folderIndexBehaviorOverrides and key each override by the folder landing-page URL:

docs.config.tsx
sidebar: {
  folderIndexBehavior: "link",
  folderIndexBehaviorOverrides: {
    "/docs/components": "toggle",
    "/docs/customization": "toggle",
  },
},

That lets one folder like Components stay toggle-only while other folders still navigate on parent click.

You can also override the behavior from the folder landing page itself. This wins over the global config and over folderIndexBehaviorOverrides:

app/docs/components/page.mdx
---
title: "Components"
sidebar:
  folderIndexBehavior: "toggle"
---

Flat Mode

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

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

Pair flat: true with folderIndexBehavior: "hidden" on a folder landing page when you want a Mintlify-style section label with child links, but no parent navigation target.

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.