Skip to Content
Fumadocs

Story

Display components with controls.

Callout

string
string

Introduction

Fumadocs Story displays and documents components. It is a simple, docs-focused alternative to Storybook, mainly designed for component libraries.

If you are new to the concept of Story, this is a short explanation from Storybook docs:

A story captures the rendered state of a UI component. It's an object with annotations that describe the component's behavior and appearance given a set of arguments.

When to use it over Storybook?

Fumadocs Story is not a replacement for Storybook. We still recommend Storybook for proper UI testing.

Installation

npm i @fumadocs/story

Add the Tailwind CSS preset:

Tailwind CSS
@import '@fumadocs/story/css/preset.css';

Add the Vite plugin:

vite.config.ts
import { defineConfig } from 'vite';
import story from '@fumadocs/story/vite';

export default defineConfig({
  plugins: [story()],
});

Create a Story

Create a story factory to store your global settings:

lib/story.ts
import { defineStoryFactory } from '@fumadocs/story/vite/client';

export const { defineStory } = defineStoryFactory();

Now create your first story:

import { defineStory } from '@/lib/story';
import { MyComponent } from './my-component';

export const story = defineStory({
  Component: MyComponent,
  args: {
    // default props (recommended)
    initial: {},
  },
});

The .story suffix is required

The plugin only transforms files matching *.story.{js,jsx,ts,tsx}. Change it with the filter option of the Vite plugin.

Render a Story

Render the story in your MDX files:

index.mdx
import { story } from '@/components/my-component.story';

<story.WithControl />

Configurations

Variants

You can provide multiple variants of the component with args:

import { defineStory } from '@/lib/story';
import { Callout } from '@/components/callout';

export const story = defineStory({
  Component: Callout,
  args: [
    {
      variant: 'Default',
      initial: {
        title: 'This is a Callout',
      },
    },
    {
      variant: 'Warning',
      initial: {
        title: 'This is a Callout',
      },
      // fixed values for props
      fixed: {
        type: 'warning',
      },
    },
  ],
});

controls

Controls are generated from the component props. Specify the control nodes to replace them:

import { defineStory } from '@/lib/story';
import { GraphView } from '@/components/graph-view';

export const story = defineStory({
  Component: GraphView,
  args: {
    controls: {
      node: {
        type: 'object',
        properties: [],
      },
    },
  },
});

frame

Components filling the viewport, like layouts, can be rendered in an iframe. The story panel scales the frame down to fit, with a button to open it in fullscreen.

components/my-layout.story.tsx
import { defineStory } from '@/lib/story';
import { MyLayout } from './my-layout.lazy';
import { Window } from './window';

export const story = defineStory({
  Component: MyLayout,
  frame: {
    src: '/story/my-layout',
    // viewport size, optional
    width: 1280,
    height: 800,
    // wraps the frame, e.g. in a window, optional
    Container: Window,
  },
});

Create a page at src to render the component. It receives the arguments from the story panel:

import { story } from '@/components/my-layout.story';

export default function Page() {
  return <story.Frame />;
}

The story loads its component on every page that renders it. Pass a lazy component instead, so only the frame loads it:

components/my-layout.lazy.tsx
import { lazy } from 'react';

export const MyLayout = lazy(() =>
  import('./my-layout').then((mod) => ({ default: mod.MyLayout })),
);

Good to know

  • The page must be on the same origin as your docs.
  • Arguments are sent with postMessage(), so they must be serializable (no functions or JSX).

Internationalization

Assuming you have configured Internationalization at UI level:

layout.shared.tsx
import { defineI18n } from 'fumadocs-core/i18n';
import { uiTranslations } from 'fumadocs-ui/i18n';
import { storyTranslations } from '@fumadocs/story/i18n';

const i18n = defineI18n({
  languages: ['en', 'cn'],
  defaultLanguage: 'en',
});

export const translations = i18n
  .translations()
  .extend(uiTranslations())
  .extend(storyTranslations())
  .add({
    cn: {
      displayName: 'Chinese',
      'Add Item(story arguments form)': '新增項目',
    },
  });

See Translations for more details.

How is this guide?

Last updated on