Skip to main content
doc0blade47/fumadocsdocumentation home
Sign in

Shared Layout Architecture

Last updated Oct 12, 2026100% grounded
Relevant source files

The following files were used as context for generating this wiki page:

Overview

Fumadocs provides a modular layout architecture built around customizable slot components, context providers, and responsive structural containers across package implementations like Radix UI Primitives and Base UI Components. This architecture decomposes complex documentation interfaces—such as the notebook and Ui Layouts Layout variants—into discrete, swappable parts including sidebars, headers, navigation panels, and page containers. By managing state centrally through React context hooks and supporting flexible slot overrides via the Fumadocs CLI, developers can easily customize individual layout pieces or construct tailored documentation experiences from the ground up without modifying core logic.

Sources: packages/radix-ui/src/layouts/notebook/page/index.tsx:77-124, packages/radix-ui/src/layouts/flux/index.tsx:33-42, packages/cli/src/commands/customise.ts:116-160

Layout Composition and Slot Contract

Slot Interfaces and Component Bindings

Layout composition across UI implementations relies on typed slot contracts that permit developers to override structural sub-components without breaking internal context bindings. In the Flux layout, the DocsSlots interface extends BaseSlots by defining custom component types for container, tabDropdown, and a structured sidebar object containing provider, trigger, root, and useSidebar hooks.

export interface DocsSlots extends BaseSlots {
  container: FC<ComponentProps<'div'>>;
  tabDropdown: FC<TabDropdownProps>;
  sidebar: {
    provider: FC<SidebarProviderProps>;
    trigger: FC<ComponentProps<'button'>>;
    root: FC<SidebarProps>;
    useSidebar: () => { collapsed: boolean; open: boolean; setOpen: (v: boolean) => void };
  };
}

Sources: packages/radix-ui/src/layouts/flux/index.tsx:33-42

Similarly, the Home layout defines HomeSlots extending BaseSlots with header and container slots, bound directly to React context via createContext and consumed through custom hooks like useHomeLayout.

export interface HomeSlots extends BaseSlots {
  header: FC<ComponentProps<'header'>>;
  container: FC<ComponentProps<'main'>>;
}

Sources: packages/radix-ui/src/layouts/home/index.tsx:22-25

Layout VariantPrimary Container SlotHeader / Navigation SlotSidebar Slot Structure
Flux Layoutslots.container (Container)renderNavigationPanel / tabDropdownslots.sidebar (provider, root, trigger, useSidebar)
Home Layoutslots.container (Container)slots.header (Header)Not applicable (nav enabled via nav.enabled)
Notebook LayoutResolved via LayoutBody / containerPropstabMode (sidebar or navbar)Sidebar options and tab resolution

Sources: packages/radix-ui/src/layouts/flux/index.tsx:33-41, packages/radix-ui/src/layouts/home/index.tsx:22-25, packages/radix-ui/src/layouts/notebook/index.tsx:13-21

Note

Layout contexts enforce strict tree containment; attempting to consume layout hooks outside their respective provider wrappers throws an explicit runtime error instructing developers to verify component nesting. Sources: packages/radix-ui/src/layouts/flux/index.tsx:69-76, packages/radix-ui/src/layouts/home/index.tsx:34-39

Tab Resolution and Layout Options

The Notebook and Flux layouts accept flexible tab configurations through the tabs property, which can be provided as an explicit array of LayoutTab objects, a GetLayoutTabsOptions configuration object, or set to false to disable tabs entirely. When passed as an object or omitted entirely, getLayoutTabs dynamically extracts tab structures from the page tree.

  const resolvedTabs = useMemo(() => {
    if (Array.isArray(tabs)) {
      return tabs;
    }
    if (typeof tabs === 'object') {
      return getLayoutTabs(tree, tabs);
    }
    if (tabs !== false) {
      return getLayoutTabs(tree);
    }
    return [];
  }, [tabs, tree]);

Sources: packages/radix-ui/src/layouts/notebook/index.tsx:38-49

Warning

Defining tabs inside sidebar options is deprecated across notebook and flux layouts; configure layout-level tabs directly on the root layout props to prevent fallback warnings. Sources: packages/radix-ui/src/layouts/flux/index.tsx:56-59, packages/radix-ui/src/layouts/notebook/index.tsx:23-28

Provider Contexts and Mode Detection

The sidebar architecture relies on provider contexts and reactive state hooks to manage visibility across viewports. SidebarProvider initializes state variables for open and collapsed, while determining the active display mode via media query evaluation.

type Mode = 'drawer' | 'full';
 
interface SidebarContext {
  open: boolean;
  setOpen: React.Dispatch<React.SetStateAction<boolean>>;
  collapsed: boolean;
  setCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
  closeOnRedirect: RefObject<boolean>;
  defaultOpenLevel: number;
  prefetch?: boolean;
  mode: Mode;
}

Sources: packages/base-ui/src/components/sidebar/base.tsx:32-64, packages/radix-ui/src/components/sidebar/base.tsx:32-64

The active layout mode switches between 'drawer' and 'full' based on a screen width breakpoint of 768px.

Mode ValueViewport ConditionBehavior
drawer(width < 768px)Mobile overlay drawer controlled by SidebarDrawerOverlay and SidebarDrawerContent
full(width >= 768px)Persistent desktop sidebar supporting collapsing and hover peek states

Sources: packages/base-ui/src/components/sidebar/base.tsx:84-84, packages/radix-ui/src/components/sidebar/base.tsx:84-84

Important

Consuming useSidebar outside of a component wrapped in SidebarProvider throws an explicit runtime error requiring a parent layout context such as <DocsLayout />. Sources: packages/base-ui/src/components/sidebar/base.tsx:114-122, packages/radix-ui/src/components/sidebar/base.tsx:114-122

Viewport and Scroll Management

Sidebar content is rendered within SidebarViewport, which nests a ScrollArea and ScrollViewport component combined with overflow containment and vertical fade gradient mask utility classes.

export function SidebarViewport({ className, ...props }: ScrollAreaProps) {
  return (
    <ScrollArea
      className={(s) =>
        cn('min-h-0 flex-1', typeof className === 'function' ? className(s) : className)
      }
      {...props}
    >
      <ScrollViewport className="p-4 overscroll-contain mask-[linear-gradient(to_bottom,transparent,white_12px,white_calc(100%-12px),transparent)]">
        {props.children}
      </ScrollViewport>
    </ScrollArea>
  );
}

Sources: packages/base-ui/src/components/sidebar/base.tsx:222-235

Tip

SidebarContent monitors pointer entry and leave events to handle collapsible sidebar peek behavior on desktop, applying a 500ms close delay when the mouse pointer leaves towards the page body rather than the viewport edge. Sources: packages/base-ui/src/components/sidebar/base.tsx:170-180, packages/radix-ui/src/components/sidebar/base.tsx:172-182

Notebook Layout Sidebar Assembly

The notebook layout sidebar implementation structures its contents using base primitives, link item renderers, and page tree configurations. SidebarProps accepts optional page tree components, banner elements, footer nodes, and a desktop collapsing flag. Sources: packages/radix-ui/src/layouts/notebook/slots/sidebar.tsx:50-60

export interface SidebarProps extends ComponentProps<'aside'> {
  components?: Partial<SidebarPageTreeComponents>;
  banner?: ReactNode | FC<ComponentProps<'div'>>;
  footer?: ReactNode | FC<ComponentProps<'div'>>;
  /**
   * Support collapsing the sidebar on desktop mode
   *
   * @defaultValue true
   */
  collapsible?: boolean;
}

Sources: packages/radix-ui/src/layouts/notebook/slots/sidebar.tsx:50-60

The SidebarContent component checks useNotebookLayout() to determine navigation mode settings, evaluating whether navMode is set to 'auto' or alternative layout parameters to calculate sticky positioning offsets against top row heights. Sources: packages/radix-ui/src/layouts/notebook/slots/sidebar.tsx:78-84

Warning

SidebarContent inside the notebook layout applies fixed positioning calculations that depend directly on --fd-docs-row-1 or --fd-docs-row-2 CSS variables; altering top row heights without matching these variable definitions breaks vertical alignment. Sources: packages/radix-ui/src/layouts/notebook/slots/sidebar.tsx:92-95

Mobile drawer toggles are governed by SidebarDrawer, which pairs Base.SidebarDrawerOverlay featuring backdrop blur effects with Base.SidebarDrawerContent styled as a fixed right- or left-hand panel constrained to 85% width up to a maximum of 380px. Sources: packages/radix-ui/src/layouts/notebook/slots/sidebar.tsx:128-146

Flux Layout Sidebar Integration

The Flux layout implements its sidebar slot by pulling menu items from useFluxLayout(), filtering out icon-only entries, and passing remaining items through SidebarLinkItem alongside page tree renderers and viewport wrappers. Sources: packages/radix-ui/src/layouts/flux/slots/sidebar.tsx:64-81

export function Sidebar({ footer, banner, components, ...rest }: SidebarProps) {
  const { menuItems } = useFluxLayout();
 
  return (
    <SidebarContent {...rest}>
      <div className="flex flex-col gap-3 p-4 pb-2 empty:hidden">{banner}</div>
      <Base.SidebarViewport viewport={{ className: '*:gap-0!' }}>
        {menuItems
          .filter((v) => v.type !== 'icon')
          .map((item, i, list) => (
            <SidebarLinkItem key={i} item={item} className={cn(i === list.length - 1 && 'mb-4')} />
          ))}
        <SidebarPageTree {...components} />
      </Base.SidebarViewport>
      {footer}
    </SidebarContent>
  );
}

Sources: packages/radix-ui/src/layouts/flux/slots/sidebar.tsx:64-81

Animated sidebar triggers switch between open and closed state icons using motion transitions: Sources: packages/radix-ui/src/layouts/flux/slots/sidebar.tsx:83-109

StateIcon ComponentAnimation Transition
OpenXIcony: '100%' initial, duration: 0.2
ClosedSidebarIcony: '100%' initial, duration: 0.2

Sources: packages/radix-ui/src/layouts/flux/slots/sidebar.tsx:83-109

Tip

The Flux sidebar modal registers a global keydown event listener via useEffectEvent that intercepts the Escape key when open is true, immediately dismissing the drawer and preventing default event propagation. Sources: packages/radix-ui/src/layouts/flux/slots/sidebar.tsx:116-127

Header and Navbar Interactions

Header Orchestration and Layout Tabs

The Header component implements sticky header navigation under the grid area #nd-subnav, incorporating backdrop blur styling and dynamic background transparency based on isNavTransparent and sidebar open states. Navigation modes are evaluated through nav.mode, defaulting to 'auto', while sidebar collapse triggers adapt their visibility depending on whether collapse rules and top navigation modes are active.

<header
  id="nd-subnav"
  data-transparent={isNavTransparent && !open}
  {...props}
  className={cn(
    'sticky [grid-area:header] flex flex-col top-(--fd-docs-row-1) z-10 backdrop-blur-sm transition-colors data-[transparent=false]:bg-fd-background/80 layout:[--fd-header-height:--spacing(14)]',
    showLayoutTabs && 'lg:layout:[--fd-header-height:--spacing(24)]',
    props.className,
  )}
>

Sources: packages/radix-ui/src/layouts/notebook/slots/header.tsx:36-45

Layout tabs are rendered via LayoutHeaderTabs when tabMode is set to 'navbar' and tabs.length exceeds zero. Active tabs are resolved against the current route pathname using isLayoutTabActive, applying primary border styling to matching route entries while supporting unlisted route filtering.

Tab PropertyType / BehaviorPurpose
titleReactNodeDisplay label inside the navbar tab anchor
urlstringTarget destination route for the layout tab
unlistedbooleanHides the tab when inactive if set to true
propsComponentProps<'a'>Additional custom attributes or className overrides

Sources: packages/radix-ui/src/layouts/notebook/slots/header.tsx:161-196

Warning

When tabMode is configured as 'navbar', the header height definition automatically scales from --spacing(14) up to --spacing(24) on large viewports (lg:layout:[--fd-header-height:--spacing(24)]), which directly impacts sticky offset calculations for content containers and sidebars. Sources: packages/radix-ui/src/layouts/notebook/slots/header.tsx:40-42

Navbar link items are processed by filtering out icon-only entries for desktop display while mapping icon items to ghost button variants with muted text styling. Custom items bypass standard rendering to execute their embedded children directly, whereas menu items delegate to popover dropdown subcomponents.

<div className="flex flex-1 items-center justify-end md:gap-2">
  <div className="flex items-center gap-6 empty:hidden max-lg:hidden">
    {navItems
      .filter((item) => item.type !== 'icon')
      .map((item, i) => (
        <NavbarLinkItem key={i} item={item} />
      ))}
  </div>
  {navItems
    .filter((item) => item.type === 'icon')
    .map((item, i) => (
      <LinkItem
        key={i}
        item={item}
        className={cn(
          buttonVariants({ size: 'icon-sm', color: 'ghost' }),
          'text-fd-muted-foreground max-lg:hidden',
        )}
        aria-label={item.label}
      >
        {item.icon}
      </LinkItem>
    ))}

Sources: packages/radix-ui/src/layouts/notebook/slots/header.tsx:86-108

Mobile layouts hide desktop link items and search triggers in favor of compact search buttons (slots.searchTrigger.sm) and sidebar drawer toggles (slots.sidebar.trigger). On desktop viewports, language selection components (slots.languageSelect.root), theme switches (slots.themeSwitch), and collapsible sidebar triggers are aligned within an end-oriented flex container.

Tip

Custom navigation components can completely bypass the default header layout by passing a custom React element directly to nav.component, triggering an immediate return statement before standard CSS classes or DOM nodes are evaluated. Sources: packages/radix-ui/src/layouts/notebook/slots/header.tsx:33-33

Page Content and Container Structures

Notebook Layout Container and Grid Template

The Container component within the notebook layout establishes a CSS grid container (#nd-notebook-layout) that manages column and row dimensions for sidebars, headers, table of contents popovers, and main article areas. It tracks sidebar collapse state via slots.sidebar?.useSidebar?.() and applies a temporary transition attribute data-column-changed={isCollapseChanged} whenever collapse toggles occur.

export function Container(props: ComponentProps<'div'>) {
  const {
    props: { nav },
    slots,
  } = useNotebookLayout();
  const pageCol =
    'calc(var(--fd-layout-width,97rem) - var(--fd-sidebar-col) - var(--fd-toc-width))';
  const { collapsed } = slots.sidebar?.useSidebar?.() ?? {};
  const [previousCollapsed, setPreviousCollapsed] = useState(collapsed);
  const isCollapseChanged = previousCollapsed !== collapsed;
 
  // will only set data attribute for an instant
  useEffect(() => {
    if (isCollapseChanged) setPreviousCollapsed(collapsed);
  }, [collapsed, isCollapseChanged]);
 

Sources: packages/radix-ui/src/layouts/notebook/slots/container.tsx:7-22, packages/base-ui/src/layouts/notebook/slots/container.tsx:7-22

The grid layout geometry calculates column widths using var(--fd-layout-width, 97rem), subtracting sidebar and TOC widths to resolve the central page column (pageCol). The template columns string enforces minimum content sizing alongside explicit widths for sidebars (var(--fd-sidebar-col)), article bounds, and table of contents columns (var(--fd-toc-width)). Depending on nav.mode, header positions swap between top grid areas and standard placement.

Grid VariableSource Formula / ValuePurpose
pageColcalc(var(--fd-layout-width,97rem) - var(--fd-sidebar-col) - var(--fd-toc-width))Dynamic column span for the central document area
--fd-sidebar-colcollapsed ? '0px' : 'var(--fd-sidebar-width)'Active width allocated to the navigation sidebar column
--fd-docs-row-1var(--fd-banner-height, 0px)Top vertical offset tracking optional banner height
--fd-docs-row-2calc(var(--fd-docs-row-1) + var(--fd-header-height))Vertical row boundary tracking sticky header placement
--fd-docs-row-3calc(var(--fd-docs-row-2) + var(--fd-toc-popover-height))Vertical boundary for TOC mobile popover regions

Sources: packages/radix-ui/src/layouts/notebook/slots/container.tsx:12-42, packages/base-ui/src/layouts/notebook/slots/container.tsx:12-42

Warning

The container enables grid template transitions only when data-column-changed is active (data-[column-changed=true]:transition-[grid-template-columns]). Direct mutation of collapsed without updating previousCollapsed via the layout effect will skip the column width animation frame. Sources: packages/radix-ui/src/layouts/notebook/slots/container.tsx:19-47, packages/base-ui/src/layouts/notebook/slots/container.tsx:19-47

Article Wrapper and Slot Integration

The page container slot (<article id="nd-page">) structures individual documentation pages across notebook and flux layouts, binding [grid-area:main] to position itself correctly inside the CSS grid. It reads full boolean state from useDocsPage() to govern maximum width constraints, applying standard typography padding and responsive gaps.

export function Container(props: ComponentProps<'article'>) {
  const { full } = useDocsPage();
 
  return (
    <article
      id="nd-page"
      data-full={full}
      {...props}
      className={cn(
        'flex flex-col [grid-area:main] px-4 py-6 gap-4 md:px-6 md:pt-8 xl:px-8 xl:pt-14 *:max-w-[900px]',
        full && '*:max-w-[1285px]',
        props.className,
      )}

Sources: packages/radix-ui/src/layouts/notebook/page/slots/container.tsx:7-19, packages/base-ui/src/layouts/notebook/page/slots/container.tsx:6-18, packages/radix-ui/src/layouts/flux/page/slots/container.tsx:7-19

Within DocsPage, optional slots such as breadcrumbs and footers are conditionally mounted alongside children inside the article container. Table of contents providers wrap the entire page structure, managing TOC item propagation based on tocEnabled and tocPopoverEnabled flags.

Layout VariantContainer Max-Width (Standard)Container Max-Width (full: true)Grid Area Binding
Notebook (radix-ui / base-ui)*:max-w-[900px]*:max-w-[1285px][grid-area:main]
Flux (radix-ui)max-w-[900px]max-w-[1200px][grid-area:main]

Sources: packages/radix-ui/src/layouts/notebook/page/slots/container.tsx:15-18, packages/base-ui/src/layouts/notebook/page/slots/container.tsx:15-16, packages/radix-ui/src/layouts/flux/page/slots/container.tsx:16-17

Tip

Setting full={true} on DocsPage overrides standard child constraints, expanding maximum child widths up to 1285px in notebook layouts and 1200px in flux layouts while automatically disabling default desktop TOC rendering unless explicitly re-enabled. Sources: packages/radix-ui/src/layouts/notebook/page/index.tsx:103-104, packages/radix-ui/src/layouts/notebook/page/slots/container.tsx:15-18, packages/radix-ui/src/layouts/flux/page/slots/container.tsx:16-17

Article Footers and Pagination Controls

The pagination footer system is driven by the Footer component, which accepts explicit pagination items or automatically discovers them using useFooterItems() and the current router pathname. When explicit items are not passed as props, the component finds the active index within the footer list by matching items against the current pathname using isActive(item.url, pathname).

export function Footer({ items, children, className, ...props }: FooterProps) {
  const footerList = useFooterItems();
  const pathname = usePathname();
  const { previous, next } = useMemo(() => {
    if (items) return items;
 
    const idx = footerList.findIndex((item) => isActive(item.url, pathname));
 
    if (idx === -1) return {};
    return {
      previous: footerList[idx - 1],
      next: footerList[idx + 1],
    };
  }, [footerList, items, pathname]);

Sources: packages/base-ui/src/layouts/flux/page/slots/footer.tsx:25-38

If no matching route is found (idx === -1), pagination objects are omitted entirely. Otherwise, previous maps to footerList[idx - 1] and next maps to footerList[idx + 1].

Warning

If isActive fails to resolve the current pathname against any entry in footerList, both previous and next pagination slots remain undefined, rendering an empty container grid unless explicit items are provided via props. Sources: packages/base-ui/src/layouts/flux/page/slots/footer.tsx:28-38

Container Layout and Pagination Item Rendering

The footer wrapper applies a container query (@container) and dynamically toggles CSS grid columns based on the presence of adjacent navigation links. When both previous and next items exist, it assigns grid-cols-2; otherwise, it falls back to a single column layout (grid-cols-1).

  return (
    <>
      <div
        className={cn(
          '@container grid gap-4',
          previous && next ? 'grid-cols-2' : 'grid-cols-1',
          className,
        )}
        {...props}
      >
        {previous && <FooterItem item={previous} index={0} />}
        {next && <FooterItem item={next} index={1} />}
      </div>
      {children}
    </>
  );
}

Sources: packages/base-ui/src/layouts/flux/page/slots/footer.tsx:40-56

Individual footer links are rendered through the FooterItem subcomponent, which uses useTranslations({ note: 'pagination' }) to supply localized fallback text (Previous Page or Next Page) when an item lacks an explicit description. The icon and text layout adapt based on the index: index 0 (previous) renders a left chevron (ChevronLeft) with standard alignment, while index 1 (next) reverses the flex-row direction, right-aligns the text, and renders a right chevron (ChevronRight).

IndexDirectionIconDefault Description FallbackCSS Alignment Classes
0 (Previous)StandardChevronLeftt('Previous Page')flex flex-col gap-2
1 (Next)Reversed (flex-row-reverse)ChevronRightt('Next Page')flex flex-col gap-2 text-end

Sources: packages/base-ui/src/layouts/flux/page/slots/footer.tsx:58-83

Was this page helpful?