Skip to main content
doc0blade47/fumadocsdocumentation home
Sign in

Base UI Components

Last updated Oct 12, 2026100% grounded
Relevant source files

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

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, packages/radix-ui/registry/index.ts:9-10, packages/base-ui/package.json:2-4, packages/radix-ui/package.json:2-4, packages/cli/src/commands/shared.ts:1-4

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

export const UIRegistries = {
  'base-ui': 'fumadocs/base-ui',
  'radix-ui': 'fumadocs/radix-ui',
};

Sources: packages/cli/src/commands/shared.ts:1-4

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, packages/base-ui/package.json:18-114

{
  "name": "@fumadocs/base-ui",
  "version": "16.10.2",
  "description": "The Base UI version of Fumadocs UI"
}

Sources: packages/base-ui/package.json:1-4

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, packages/radix-ui/package.json:94-95, packages/radix-ui/package.json:116-117

{
  "name": "fumadocs-ui",
  "version": "16.10.2",
  "description": "The Radix UI version of Fumadocs UI"
}

Sources: packages/radix-ui/package.json:1-4

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, packages/base-ui/registry/index.ts:15-35, packages/base-ui/registry/index.ts:159-272, packages/radix-ui/registry/index.ts:9-14, packages/radix-ui/registry/index.ts:15-35, packages/radix-ui/registry/index.ts:159-272

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

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, packages/radix-ui/registry/index.ts:8-14

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

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).

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

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

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.

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

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.

KeyActionBehavior
ArrowDownIncrement IndexMoves active focus down; wraps to index 0 at the end of the list.
ArrowUpDecrement IndexMoves active focus up; wraps to the last item when reaching the top.
EnterSelect ItemInvokes onSelect with the currently active search item object.

Sources: packages/base-ui/src/components/dialog/search.tsx:342-358

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

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.

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

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

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

ComponentBase UI PrimitiveCSS Classes and Styling Behavior
AccordionPrimitive.Rootdivide-y divide-fd-border overflow-hidden rounded-lg border bg-fd-card
AccordionItemPrimitive.ItemPasses props directly without extra wrapper classes.
AccordionHeaderPrimitive.Headerscroll-m-24 not-prose flex flex-row items-center text-fd-card-foreground font-medium has-focus-visible:bg-fd-accent
AccordionTriggerPrimitive.Triggergroup flex flex-1 items-center gap-2 px-3 py-2.5 text-start focus-visible:outline-none
AccordionContentPrimitive.Panelh-(--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

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.

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

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

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.

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

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

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

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.

function escapeValue(v: string): string {
  return v.toLowerCase().replace(/\s/, '-');
}

Sources: packages/base-ui/src/components/tabs.tsx:202-204

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.

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

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

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.

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

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

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.

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

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

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.

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

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

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.

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

Sources: packages/base-ui/src/components/card.tsx:22-24

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

PropertyTypeDescription
iconReactNodeOptional visual indicator rendered inside a styled container block above the title
titleReactNodePrimary card heading text rendered within an h3 element
descriptionReactNodeOptional secondary description text rendered in muted styling
hrefstringTarget URL for anchor navigation, transforming the container into a link component
externalbooleanFlag indicating whether the link targets an external destination

Sources: packages/base-ui/src/components/card.tsx:13-20

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

Table of Contents Integration Primitives

Anchor Providers and Scroll Observer Bindings

The 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.

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

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.

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

Function / ComponentReturn Type / SignatureDescription
useTOCItemsPrimitive.TOCItemType[]Consumes TOCContext to return the array of table of contents items
TOCProvider(props: TOCProviderProps) => ReactNodeContext provider wrapping AnchorProvider and TOCContext
TOCScrollArea(props: ComponentProps<'div'>) => ReactNodeScrollable container linking viewRef to Primitive.ScrollProvider via mergeRefs

Sources: packages/base-ui/src/components/toc/index.tsx:9-41

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

Was this page helpful?