---
title: "Base UI Components"
description: "Base UI Components provides the core interactive building blocks, layout primitives, and styling integration for Fumadocs UI, enabling flexible documentation site construction across multiple JavaS..."
last_updated: "2026-10-12T07:53:59.981993+00:00"
canonical_url: "https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/ui-layouts/base-ui-components"
---

<details>
<summary>Relevant source files</summary>

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

- [packages/base-ui/registry/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/registry/index.ts)
- [packages/radix-ui/registry/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/registry/index.ts)
- [packages/base-ui/package.json](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/package.json)
- [packages/base-ui/src/components/dialog/search.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/dialog/search.tsx)
- [packages/api-docs/src/components/dialog.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/api-docs/src/components/dialog.tsx)
- [packages/base-ui/src/layouts/notebook/slots/sidebar.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/layouts/notebook/slots/sidebar.tsx)
- [packages/radix-ui/src/layouts/notebook/slots/sidebar.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/layouts/notebook/slots/sidebar.tsx)
- [packages/sanity/registry/base.component.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/registry/base.component.tsx)
- [packages/base-ui/src/components/tabs.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/tabs.tsx)
- [packages/base-ui/src/components/card.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/card.tsx)
- [packages/base-ui/src/components/accordion.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/accordion.tsx)
- [packages/cli/src/commands/shared.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/commands/shared.ts)
- [packages/radix-ui/src/components/dialog/search.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/dialog/search.tsx)
- [packages/base-ui/src/components/sidebar/base.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/sidebar/base.tsx)
- [packages/base-ui/src/layouts/home/navbar.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/layouts/home/navbar.tsx)
- [packages/base-ui/src/layouts/flux/slots/sidebar.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/layouts/flux/slots/sidebar.tsx)
- [packages/base-ui/src/components/toc/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/toc/index.tsx)
- [packages/radix-ui/package.json](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/package.json)
- [packages/python/src/components/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/python/src/components/index.tsx)
- [packages/openapi/src/ui/base.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/ui/base.tsx)
- [packages/base-ui/src/provider/waku.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/provider/waku.tsx)
- [packages/radix-ui/src/components/card.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/card.tsx)
- [packages/radix-ui/src/components/sidebar/base.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/sidebar/base.tsx)
- [packages/base-ui/src/layouts/shared/client.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/layouts/shared/client.tsx)
- [packages/base-ui/src/provider/react-router.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/provider/react-router.tsx)
- [packages/base-ui/src/provider/next.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/provider/next.tsx)
- [packages/base-ui/src/provider/tanstack.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/provider/tanstack.tsx)
- [packages/base-ui/src/components/ui/navigation-menu.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/ui/navigation-menu.tsx)
- [packages/base-ui/src/components/ui/accordion.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/ui/accordion.tsx)
- [packages/base-ui/src/layouts/flux/page/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/layouts/flux/page/index.tsx)
</details>

## Overview

Base UI Components provides the core interactive building blocks, layout primitives, and styling integration for Fumadocs UI, enabling flexible documentation site construction across multiple JavaScript UI toolkits and routing frameworks. Built around modular registry architectures and headless design patterns, the package supports dual distribution via `@fumadocs/base-ui` and `@fumadocs/radix-ui` registries. It solves the challenge of maintaining accessible, framework-agnostic documentation layouts, keyboard-navigable search dialogs, collapsible navigation trees, and synchronized tab containers through encapsulated React components and utility hooks.

Sources: [packages/base-ui/registry/index.ts:9-10](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/registry/index.ts#L9-L10), [packages/radix-ui/registry/index.ts:9-10](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/registry/index.ts#L9-L10), [packages/base-ui/package.json:2-4](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/package.json#L2-L4), [packages/radix-ui/package.json:2-4](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/package.json#L2-L4), [packages/cli/src/commands/shared.ts:1-4](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/commands/shared.ts#L1-L4)

## Package Architecture and Registry Discovery

### Registry Mapping and Package Configuration

The CLI command configuration maps UI registries to specific identifier strings through the `UIRegistries` object, binding `'base-ui'` to `'fumadocs/base-ui'` and `'radix-ui'` to `'fumadocs/radix-ui'`.

Sources: [packages/cli/src/commands/shared.ts:1-4](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/commands/shared.ts#L1-L4)

```typescript
export const UIRegistries = {
  'base-ui': 'fumadocs/base-ui',
  'radix-ui': 'fumadocs/radix-ui',
};
```
Sources: [packages/cli/src/commands/shared.ts:1-4](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/commands/shared.ts#L1-L4)

Both packages share version `16.10.2` and export modular entry points, yet their distribution layouts diverge based on underlying primitive dependencies. `@fumadocs/base-ui` defines package exports for base UI components, providers, and layout slots while targeting `@fumadocs/base-ui`.

Sources: [packages/base-ui/package.json:2-3](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/package.json#L2-L3), [packages/base-ui/package.json:18-114](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/package.json#L18-L114)

```json
{
  "name": "@fumadocs/base-ui",
  "version": "16.10.2",
  "description": "The Base UI version of Fumadocs UI"
}
```
Sources: [packages/base-ui/package.json:1-4](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/package.json#L1-L4)

Conversely, `@fumadocs-ui` (published from the `radix-ui` package directory) exposes legacy layout and sidebar entries alongside equivalent Radix UI primitive paths such as `./utils/get-sidebar-tabs` and `./components/tabs.unstyled`.

Sources: [packages/radix-ui/package.json:1-4](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/package.json#L1-L4), [packages/radix-ui/package.json:94-95](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/package.json#L94-L95), [packages/radix-ui/package.json:116-117](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/package.json#L116-L117)

```json
{
  "name": "fumadocs-ui",
  "version": "16.10.2",
  "description": "The Radix UI version of Fumadocs UI"
}
```
Sources: [packages/radix-ui/package.json:1-4](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/package.json#L1-L4)

### Component Registries and Layout Definitions

The registry definitions for both `base-ui` and `radix-ui` construct a component collection by spreading `commonComponents` and evaluating `findSlotComponents(dir)`, subsequently registering unlisted layout blocks including `layouts/sidebar`, `layouts/shared`, `layouts/docs`, `layouts/flux`, `layouts/notebook`, `layouts/home`, and modular component entries like `accordion`, `github-info`, `banner`, `callout`, `card`, `codeblock`, `files`, `heading`, `image-zoom`, `inline-toc`, `steps`, and `tabs`.

Sources: [packages/base-ui/registry/index.ts:9-14](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/registry/index.ts#L9-L14), [packages/base-ui/registry/index.ts:15-35](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/registry/index.ts#L15-L35), [packages/base-ui/registry/index.ts:159-272](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/registry/index.ts#L159-L272), [packages/radix-ui/registry/index.ts:9-14](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/registry/index.ts#L9-L14), [packages/radix-ui/registry/index.ts:15-35](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/registry/index.ts#L15-L35), [packages/radix-ui/registry/index.ts:159-272](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/registry/index.ts#L159-L272)

```typescript
export const registry: Registry = {
  name: 'fumadocs/base-ui',
  dir,
  components: [
    ...commonComponents,
    ...(await findSlotComponents(dir)),
    {
      name: 'layouts/sidebar',
      unlisted: true,
      files: [
        {
          type: 'components',
          path: 'components/sidebar/base.tsx',
          target: '<dir>/docs-sidebar/base.tsx',
        },
        {
          type: 'components',
          path: 'components/sidebar/page-tree.tsx',
          target: '<dir>/docs-sidebar/page-tree.tsx',
        },
        {
          type: 'components',
          path: 'components/sidebar/link-item.tsx',
          target: '<dir>/docs-sidebar/link-item.tsx',
        },
      ],
    },
```
Sources: [packages/base-ui/registry/index.ts:9-34](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/registry/index.ts#L9-L34)

> [!NOTE]
> File order matters in the shadcn CLI registry configuration when writing import paths on consumer codebases, making the sequence of layout and component registrations critical for proper module resolution.
> Sources: [packages/base-ui/registry/index.ts:8-14](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/registry/index.ts#L8-L14), [packages/radix-ui/registry/index.ts:8-14](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/registry/index.ts#L8-L14)

## Search Dialog Component Primitives

### List Virtualization and Keyboard Navigation

The search dialog lifecycle and presentation are composed around primitive wrappers, including `SearchDialogOverlay`, `SearchDialogContent`, `SearchDialogList`, and `SearchDialogListItem`. These components manage portal mounting, backdrop blur effects, animated height adjustments via `ResizeObserver`, and keyboard navigation handlers.

Sources: [packages/base-ui/src/components/dialog/search.tsx:268-283](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/dialog/search.tsx#L268-L283)

`SearchDialogContent` wraps content inside `Dialog.Portal` and `Dialog.Popup`, fixing the container's position to the center of the screen with responsive top offset calculations (`md:top-[calc(50%-250px)]`), maximum width sizing (`max-w-screen-sm`), overflow clipping, and entrance/exit animation hooks (`data-open:animate-fd-dialog-in`, `data-closed:animate-fd-dialog-out`).

```tsx
export function SearchDialogContent({
  children,
  className,
  ...props
}: ComponentProps<typeof Dialog.Popup>) {
  const t = useTranslations({ note: 'search dialog' });

  return (
    <Dialog.Portal>
      <Dialog.Popup
        id="fd-search-dialog-content"
        aria-describedby={undefined}
        {...props}
        className={(s) =>
          cn(
            'fixed left-1/2 top-4 md:top-[calc(50%-250px)] z-50 w-[calc(100%-1rem)] max-w-screen-sm -translate-x-1/2 rounded-xl border bg-fd-popover text-fd-popover-foreground shadow-2xl overflow-hidden data-closed:animate-fd-dialog-out data-open:animate-fd-dialog-in focus-visible:outline-none',
            '*:border-b *:has-[+:last-child[data-empty=true]]:border-b-0 *:data-[empty=true]:border-b-0 *:last:border-b-0',
            typeof className === 'function' ? className(s) : className,
          )
        }
      >
        <Dialog.Title className="hidden">{t('Search')}</Dialog.Title>
        {children}
      </Dialog.Popup>
    </Dialog.Portal>
  );
}
```
Sources: [packages/base-ui/src/components/dialog/search.tsx:285-311](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/dialog/search.tsx#L285-L311)

> [!NOTE]
> `SearchDialogContent` explicitly forces `aria-describedby={undefined}` on the underlying `Dialog.Popup` component to suppress screen reader warnings regarding missing descriptive node elements when rendering dynamic search records.
> Sources: [packages/base-ui/src/components/dialog/search.tsx:295-296](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/dialog/search.tsx#L295-L296)

`SearchDialogList` manages active item selection state, observes container dimensions via `ResizeObserver` to dynamically apply a `--fd-animated-height` CSS property for height transitions, and binds window- or content-level keydown listeners for keyboard traversal.

```tsx
export function SearchDialogList({
  items = null,
  Empty = () => (
    <div className="py-12 text-center text-sm text-fd-muted-foreground">
      <T text="No results found" note="search dialog" />
    </div>
  ),
  Item = (props) => <SearchDialogListItem {...props} />,
  ...props
}: Omit<ComponentProps<'div'>, 'children'> & {
  items: SearchItemType[] | null | undefined;
  /**
   * Renderer for empty list UI
   */
  Empty?: () => ReactNode;
  /**
   * Renderer for items
   */
  Item?: (props: { item: SearchItemType; onClick: () => void }) => ReactNode;
}) {
  const ref = useRef<HTMLDivElement>(null);
  const { onSelect } = useSearch();
  const [active, setActive] = useState<string | null>(() =>
    items && items.length > 0 ? items[0].id : null,
  );

  const onKey = useEffectEvent((e: KeyboardEvent) => {
    if (!items || e.isComposing) return;

    if (e.key === 'ArrowDown' || e.key == 'ArrowUp') {
      let idx = items.findIndex((item) => item.id === active);
      if (idx === -1) idx = 0;
      else if (e.key === 'ArrowDown') idx++;
      else idx--;

      setActive(items.at(idx % items.length)?.id ?? null);
      e.preventDefault();
    }

    if (e.key === 'Enter') {
      const selected = items.find((item) => item.id === active);

      if (selected) onSelect(selected);
      e.preventDefault();
    }
  });

  useEffect(() => {
```
Sources: [packages/base-ui/src/components/dialog/search.tsx:313-360](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/dialog/search.tsx#L313-L360)

The keyboard event handler processes specific navigation keys to modify the active selection index modulo the total item length, and triggers selection upon pressing `Enter`.

| Key | Action | Behavior |
| :--- | :--- | :--- |
| `ArrowDown` | Increment Index | Moves active focus down; wraps to index `0` at the end of the list. |
| `ArrowUp` | Decrement Index | Moves active focus up; wraps to the last item when reaching the top. |
| `Enter` | Select Item | Invokes `onSelect` with the currently active search item object. |

Sources: [packages/base-ui/src/components/dialog/search.tsx:342-358](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/dialog/search.tsx#L342-L358)

> [!WARNING]
> Keyboard listeners check `e.isComposing` before evaluating arrow keys or enter inputs to prevent capturing keystrokes while users are actively composing characters via an Input Method Editor (IME).
> Sources: [packages/base-ui/src/components/dialog/search.tsx:340](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/dialog/search.tsx#L340)

## Collapsible Accordions and Trigger Controls

### Accordion Structure and State Synchronization

The accordion components are built on top of `@base-ui/react/accordion` primitives, providing accessible disclosure containers with animated height transitions and hash-based anchor linking. The exported `Accordions` wrapper component maintains the expanded item values in local state while reading the initial `window.location.hash` on mount to automatically expand targeted items.

```tsx
export function Accordions({
  ref,
  className,
  defaultValue,
  ...props
}: ComponentProps<typeof Root>) {
  const rootRef = useRef<HTMLDivElement>(null);
  const composedRef = mergeRefs(ref, rootRef);
  const [value, setValue] = useState<unknown[]>(defaultValue ?? []);

  useEffect(() => {
    const id = window.location.hash.substring(1);
    const element = rootRef.current;
    if (!element || id.length === 0) return;

    const selected = document.getElementById(id);
    if (!selected || !element.contains(selected)) return;
    const value = selected.getAttribute('data-accordion-value');

    if (value) setValue((prev) => [value, ...prev]);
  }, []);
```
Sources: [packages/base-ui/src/components/accordion.tsx:18-38](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/accordion.tsx#L18-L38)

> [!NOTE]
> When an element ID matches the URL hash upon initial page load, the accordion component verifies that the target DOM element exists inside the accordion container before prepending its `data-accordion-value` into the expanded state array.
> Sources: [packages/base-ui/src/components/accordion.tsx:28-38](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/accordion.tsx#L28-L38)

The styling and layout structure for the accordion components are mapped through UI wrapper functions defined in the UI layer.

| Component | Base UI Primitive | CSS Classes and Styling Behavior |
| :--- | :--- | :--- |
| `Accordion` | `Primitive.Root` | `divide-y divide-fd-border overflow-hidden rounded-lg border bg-fd-card` |
| `AccordionItem` | `Primitive.Item` | Passes props directly without extra wrapper classes. |
| `AccordionHeader` | `Primitive.Header` | `scroll-m-24 not-prose flex flex-row items-center text-fd-card-foreground font-medium has-focus-visible:bg-fd-accent` |
| `AccordionTrigger` | `Primitive.Trigger` | `group flex flex-1 items-center gap-2 px-3 py-2.5 text-start focus-visible:outline-none` |
| `AccordionContent` | `Primitive.Panel` | `h-(--accordion-panel-height) overflow-hidden transition-[height] ease-out data-[ending-style]:h-0 data-[starting-style]:h-0` |

Sources: [packages/base-ui/src/components/ui/accordion.tsx:8-84](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/ui/accordion.tsx#L8-L84)

### Triggers, Content Panels, and Copy Actions

Individual `Accordion` sections combine an `AccordionItem`, `AccordionHeader`, `AccordionTrigger`, and `AccordionContent` with an optional link-copy button. The trigger incorporates a rotating chevron indicator that reacts to panel opening states via group data attributes.

```tsx
export function AccordionTrigger({
  className,
  children,
  ...props
}: ComponentProps<typeof Primitive.Trigger>) {
  return (
    <Primitive.Trigger
      className={(s) =>
        cn(
          'group flex flex-1 items-center gap-2 px-3 py-2.5 text-start focus-visible:outline-none',
          typeof className === 'function' ? className(s) : className,
        )
      }
      {...props}
    >
      <ChevronRight className="size-4 shrink-0 text-fd-muted-foreground transition-transform duration-200 group-data-[panel-open]:rotate-90" />
      {children}
    </Primitive.Trigger>
  );
```
Sources: [packages/base-ui/src/components/ui/accordion.tsx:46-64](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/ui/accordion.tsx#L46-L64)

```tsx
export function AccordionContent({
  className,
  children,
  ...props
}: ComponentProps<typeof Primitive.Panel>) {
  return (
    <Primitive.Panel
      className={(s) =>
        cn(
          'h-(--accordion-panel-height) overflow-hidden transition-[height] ease-out data-[ending-style]:h-0 data-[starting-style]:h-0',
          typeof className === 'function' ? className(s) : className,
        )
      }
      {...props}
    >
      {children}
    </Primitive.Panel>
  );
```
Sources: [packages/base-ui/src/components/ui/accordion.tsx:67-84](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/ui/accordion.tsx#L67-L84)

When an `id` prop is supplied to the `Accordion` component, a `CopyButton` is rendered inside the header alongside the trigger. This button uses `useCopyButton` to write a direct URL hash link to the clipboard and temporarily toggles a check icon upon successful copying.

```tsx
export function Accordion({
  title,
  id,
  value = String(title),
  children,
  ...props
}: Omit<ComponentProps<typeof AccordionItem>, 'value' | 'title'> & {
  title: string | ReactNode;
  value?: string;
}) {
  return (
    <AccordionItem value={value} {...props}>
      <AccordionHeader id={id} data-accordion-value={value}>
        <AccordionTrigger>{title}</AccordionTrigger>
        {id ? <CopyButton id={id} /> : null}
      </AccordionHeader>
      <AccordionContent hiddenUntilFound>
        <div className="px-4 pb-2 text-[0.9375rem] prose-no-margin [&[hidden]:not([hidden='until-found'])]:hidden">
          {children}
        </div>
      </AccordionContent>
    </AccordionItem>
  );
}
```
Sources: [packages/base-ui/src/components/accordion.tsx:56-79](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/accordion.tsx#L56-L79)

```tsx
function CopyButton({ id }: { id: string }) {
  const t = useTranslations({ note: 'accordion' });
  const [checked, onClick] = useCopyButton(() => {
    const url = new URL(window.location.href);
    url.hash = id;

    return navigator.clipboard.writeText(url.toString());
  });

  return (
    <button
      type="button"
      aria-label={t('Copy Link', { note: 'aria-label' })}
      className={cn(
        buttonVariants({
          color: 'ghost',
          className: 'text-fd-muted-foreground me-2',
        }),
      )}
      onClick={onClick}
    >
      {checked ? <Check className="size-3.5" /> : <LinkIcon className="size-3.5" />}
    </button>
  );
}
```
Sources: [packages/base-ui/src/components/accordion.tsx:81-105](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/accordion.tsx#L81-L105)

> [!CAUTION]
> `AccordionContent` specifies `hiddenUntilFound` to allow browser find-in-page features to automatically reveal text content hidden inside collapsed accordion panels.
> Sources: [packages/base-ui/src/components/accordion.tsx:72](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/accordion.tsx#L72)

## Tabs Containers and Active State Sync

### State Synchronization and Simple Mode

The `Tabs` component supports both advanced Radix-style usage and simple mode via the `items` property. In simple mode, string identifiers passed to `items` are transformed into normalized values using the `escapeValue` helper function, which converts strings to lowercase and replaces whitespace characters with hyphens.

```tsx
function escapeValue(v: string): string {
  return v.toLowerCase().replace(/\s/, '-');
}
```
Sources: [packages/base-ui/src/components/tabs.tsx:202-204](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/tabs.tsx#L202-L204)

The `Tabs` root component initializes its state using `defaultIndex` or `defaultValue`, validating any value updates against the item collection when simple mode `items` are defined.

```tsx
export function Tabs({
  ref,
  className,
  items,
  label,
  defaultIndex = 0,
  defaultValue = items ? escapeValue(items[defaultIndex]) : undefined,
  ...props
}: TabsProps) {
  const [value, setValue] = useState(defaultValue);
  const collection = useMemo<CollectionKey[]>(() => [], []);

  return (
    <Unstyled.Tabs
      ref={ref}
      className={(s) =>
        cn(
          'flex flex-col overflow-hidden rounded-xl border bg-fd-secondary my-4',
          typeof className === 'function' ? className(s) : className,
        )
      }
      value={value}
      onValueChange={(v: string) => {
        if (items && !items.some((item) => escapeValue(item) === v)) return;
        setValue(v);
      }}
      {...props}
    >
```
Sources: [packages/base-ui/src/components/tabs.tsx:86-113](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/tabs.tsx#L86-L113)

> [!WARNING]
> When `items` are provided, `onValueChange` explicitly blocks value updates if the incoming value does not match an escaped item string in the collection list.
> Sources: [packages/base-ui/src/components/tabs.tsx:108-111](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/tabs.tsx#L108-L111)

### Collection Index Tracking and Context

Child panels coordinate their active states through a React context that holds the `items` array and a mutable `collection` key array. The `useCollectionIndex` hook registers component render order to automatically assign indices for tab items lacking explicit value props.

```tsx
const TabsContext = createContext<{
  items?: string[];
  collection: CollectionKey[];
} | null>(null);

function useTabContext() {
  const ctx = useContext(TabsContext);
  if (!ctx) throw new Error('You must wrap your component in <Tabs>');
  return ctx;
}
```
Sources: [packages/base-ui/src/components/tabs.tsx:41-50](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/tabs.tsx#L41-L50)

```tsx
function useCollectionIndex() {
  const key = useId();
  const { collection } = useTabContext();

  useEffect(() => {
    return () => {
      const idx = collection.indexOf(key);
      if (idx !== -1) collection.splice(idx, 1);
    };
  }, [key, collection]);

  if (!collection.includes(key)) collection.push(key);
  return collection.indexOf(key);
}
```
Sources: [packages/base-ui/src/components/tabs.tsx:184-197](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/tabs.tsx#L184-L197)

The `Tab` component resolves its target value by falling back to the collection index position within the `items` list if no explicit `value` prop is supplied.

```tsx
export function Tab({ value, ...props }: TabProps) {
  const { items } = useTabContext();
  const resolved =
    value ??
    // eslint-disable-next-line react-hooks/rules-of-hooks -- `value` is not supposed to change
    items?.at(useCollectionIndex());
  if (!resolved)
    throw new Error(
      'Failed to resolve tab `value`, please pass a `value` prop to the Tab component.',
    );

  return (
    <TabsContent value={escapeValue(resolved)} {...props}>
      {props.children}
    </TabsContent>
  );
}
```
Sources: [packages/base-ui/src/components/tabs.tsx:138-154](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/tabs.tsx#L138-L154)

> [!NOTE]
> `TabsContent` forces `keepMounted` to maintain DOM nodes while inactive, and hides unselected panels using the `data-[inactive]:hidden` attribute selector.
> Sources: [packages/base-ui/src/components/tabs.tsx:156-176](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/tabs.tsx#L156-L176)

## Interactive Cards and Grid Layouts

### Cards Grid Collection and Container Queries

The `Cards` wrapper component organizes card collections into a responsive two-column grid layout utilizing container query units. It renders a standard `div` element spreading all inbound `HTMLAttributes<HTMLDivElement>` props while applying utility classes for grid placement, column count, and container query context.

```tsx
export function Cards(props: HTMLAttributes<HTMLDivElement>) {
  return (
    <div {...props} className={cn('grid grid-cols-2 gap-3 @container', props.className)}>
      {props.children}
    </div>
  );
}
```
Sources: [packages/base-ui/src/components/card.tsx:5-11](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/card.tsx#L5-L11)

> [!NOTE]
> The `@container` class on `Cards` enables container query breakpoints so child cards can respond dynamically to the grid container width rather than the global viewport width.
> Sources: [packages/base-ui/src/components/card.tsx:7](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/card.tsx#L7)

### Card Presentation and Anchor Link Integration

Individual `Card` components switch their underlying HTML tag dynamically based on the presence of an `href` property. When `href` is supplied, the card uses `fumadocs-core/link` (`Link`); otherwise, it defaults to a standard `'div'` element.

```tsx
export function Card({ icon, title, description, ...props }: CardProps) {
  const E = props.href ? Link : 'div';

```
Sources: [packages/base-ui/src/components/card.tsx:22-24](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/card.tsx#L22-L24)

```tsx
return (
  <E
    {...props}
    data-card
    className={cn(
      'block rounded-xl border bg-fd-card p-4 text-fd-card-foreground transition-colors @max-lg:col-span-full',
      props.href && 'hover:bg-fd-accent/80',
      props.className,
    )}
  >
```
Sources: [packages/base-ui/src/components/card.tsx:25-34](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/card.tsx#L25-L34)

| Property | Type | Description |
| --- | --- | --- |
| `icon` | `ReactNode` | Optional visual indicator rendered inside a styled container block above the title |
| `title` | `ReactNode` | Primary card heading text rendered within an `h3` element |
| `description` | `ReactNode` | Optional secondary description text rendered in muted styling |
| `href` | `string` | Target URL for anchor navigation, transforming the container into a link component |
| `external` | `boolean` | Flag indicating whether the link targets an external destination |

Sources: [packages/base-ui/src/components/card.tsx:13-20](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/card.tsx#L13-L20)

> [!WARNING]
> Cards configured with an `href` automatically gain hover background styles (`hover:bg-fd-accent/80`) and span the full grid width when container queries drop below large viewports (`@max-lg:col-span-full`).
> Sources: [packages/base-ui/src/components/card.tsx:30-31](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/card.tsx#L30-L31)

## Table of Contents Integration Primitives

### Anchor Providers and Scroll Observer Bindings

The [Table of Contents](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/ui-layouts/table-of-contents) implementation centers on `TOCProvider`, which wraps `fumadocs-core/toc`'s `AnchorProvider` while concurrently exposing the parsed table of contents items array through a React context called `TOCContext`. Consumer components access these structured items via the `useTOCItems` hook, or consume active tracking primitives such as `useActiveAnchor`, `useActiveAnchors`, and `useItems` re-exported directly from the core primitive.

```tsx
const TOCContext = createContext<Primitive.TOCItemType[]>([]);

export function useTOCItems(): Primitive.TOCItemType[] {
  return use(TOCContext);
}

export type TOCProviderProps = Primitive.AnchorProviderProps;

export const { useActiveAnchor, useActiveAnchors, useItems } = Primitive;

export function TOCProvider({ toc, children, ...props }: TOCProviderProps) {
  return (
    <TOCContext value={toc}>
      <Primitive.AnchorProvider toc={toc} {...props}>
        {children}
      </Primitive.AnchorProvider>
    </TOCContext>
  );
}
```
Sources: [packages/base-ui/src/components/toc/index.tsx:7-25](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/toc/index.tsx#L7-L25)

### Scroll Areas and Element Ref Merging

The `TOCScrollArea` component manages scrollable viewport containers for sidebars by establishing a local `viewRef` bound to a styled `div`. It uses `mergeRefs` to combine this internal view reference with any inbound `ref` prop from parent consumers, ensuring that scroll observers and DOM bindings attach correctly without overwriting user-provided ref objects.

```tsx
export function TOCScrollArea({ ref, className, ...props }: ComponentProps<'div'>) {
  const viewRef = useRef<HTMLDivElement>(null);

  return (
    <div
      ref={mergeRefs(viewRef, ref)}
      className={cn(
        'relative min-h-0 text-sm ms-px overflow-auto [scrollbar-width:none] mask-[linear-gradient(to_bottom,transparent,white_16px,white_calc(100%-16px),transparent)] py-3',
        className,
      )}
      {...props}
    >
      <Primitive.ScrollProvider containerRef={viewRef}>{props.children}</Primitive.ScrollProvider>
    </div>
  );
```
Sources: [packages/base-ui/src/components/toc/index.tsx:27-41](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/toc/index.tsx#L27-L41)

| Function / Component | Return Type / Signature | Description |
| --- | --- | --- |
| `useTOCItems` | `Primitive.TOCItemType[]` | Consumes `TOCContext` to return the array of table of contents items |
| `TOCProvider` | `(props: TOCProviderProps) => ReactNode` | Context provider wrapping `AnchorProvider` and `TOCContext` |
| `TOCScrollArea` | `(props: ComponentProps<'div'>) => ReactNode` | Scrollable container linking `viewRef` to `Primitive.ScrollProvider` via `mergeRefs` |

Sources: [packages/base-ui/src/components/toc/index.tsx:9-41](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/toc/index.tsx#L9-L41)

> [!NOTE]
> `TOCScrollArea` hides native scrollbars using `[scrollbar-width:none]` and applies a vertical linear gradient mask to fade out overflowing heading items smoothly at the top and bottom edges.
> Sources: [packages/base-ui/src/components/toc/index.tsx:34](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/toc/index.tsx#L34)

## Related

- [Radix UI Primitives](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/ui-layouts/radix-ui-primitives)
- [Shared Layout Architecture](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/ui-layouts/shared-layout-architecture)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/llms.txt) for all pages in this wiki.
