---
title: "Story Visualizer"
description: "The Story Visualizer is a sophisticated documentation subsystem designed to bridge the gap between static component documentation and interactive development environments. Its primary purpose is to..."
last_updated: "2026-07-02T09:46:39.49663+00:00"
canonical_url: "https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/developer-tools/story-visualizer"
---

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

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

- [packages/preview/src/pages/[...slugs].tsx](https://github.com/blade47/fumadocs/blob/main/packages/preview/src/pages/%5B...slugs%5D.tsx)
- [packages/story/src/client/with-control.tsx](https://github.com/blade47/fumadocs/blob/main/packages/story/src/client/with-control.tsx)
- [packages/story/src/webpack/story.ts](https://github.com/blade47/fumadocs/blob/main/packages/story/src/webpack/story.ts)
- [packages/story/src/type-tree/builder.ts](https://github.com/blade47/fumadocs/blob/main/packages/story/src/type-tree/builder.ts)
- [packages/story/src/index.tsx](https://github.com/blade47/fumadocs/blob/main/packages/story/src/index.tsx)
- [packages/api-docs/src/components/schema/client.tsx](https://github.com/blade47/fumadocs/blob/main/packages/api-docs/src/components/schema/client.tsx)
- [packages/mdx/src/webpack/meta.ts](https://github.com/blade47/fumadocs/blob/main/packages/mdx/src/webpack/meta.ts)
- [packages/story/src/vite/index.ts](https://github.com/blade47/fumadocs/blob/main/packages/story/src/vite/index.ts)
- [packages/preview/src/lib/source/index.ts](https://github.com/blade47/fumadocs/blob/main/packages/preview/src/lib/source/index.ts)
- [packages/cli/src/commands/customise.ts](https://github.com/blade47/fumadocs/blob/main/packages/cli/src/commands/customise.ts)
- [packages/preview/src/config/load-runtime.ts](https://github.com/blade47/fumadocs/blob/main/packages/preview/src/config/load-runtime.ts)
- [packages/radix-ui/package.json](https://github.com/blade47/fumadocs/blob/main/packages/radix-ui/package.json)
- [packages/asyncapi/src/ui/components/markdown.tsx](https://github.com/blade47/fumadocs/blob/main/packages/asyncapi/src/ui/components/markdown.tsx)
- [packages/story/src/client/compiled.tsx](https://github.com/blade47/fumadocs/blob/main/packages/story/src/client/compiled.tsx)
- [packages/radix-ui/src/components/dynamic-codeblock.core.tsx](https://github.com/blade47/fumadocs/blob/main/packages/radix-ui/src/components/dynamic-codeblock.core.tsx)
- [packages/preview/src/components/hot-reload.tsx](https://github.com/blade47/fumadocs/blob/main/packages/preview/src/components/hot-reload.tsx)
- [packages/openapi/src/ui/components/markdown.tsx](https://github.com/blade47/fumadocs/blob/main/packages/openapi/src/ui/components/markdown.tsx)
- [packages/preview/src/components/markdown.tsx](https://github.com/blade47/fumadocs/blob/main/packages/preview/src/components/markdown.tsx)
- [packages/preview/src/pages/_root.tsx](https://github.com/blade47/fumadocs/blob/main/packages/preview/src/pages/_root.tsx)
- [packages/story/src/utils/transform.ts](https://github.com/blade47/fumadocs/blob/main/packages/story/src/utils/transform.ts)
- [packages/core/src/mdx-plugins/remark-llms.runtime.ts](https://github.com/blade47/fumadocs/blob/main/packages/core/src/mdx-plugins/remark-llms.runtime.ts)
- [packages/openapi/src/ui/operation/context.tsx](https://github.com/blade47/fumadocs/blob/main/packages/openapi/src/ui/operation/context.tsx)
- [packages/story/src/utils/generate.ts](https://github.com/blade47/fumadocs/blob/main/packages/story/src/utils/generate.ts)
- [packages/preview/src/config/index.ts](https://github.com/blade47/fumadocs/blob/main/packages/preview/src/config/index.ts)
- [packages/mdx/src/webpack/index.ts](https://github.com/blade47/fumadocs/blob/main/packages/mdx/src/webpack/index.ts)
- [packages/story/src/vite/client.tsx](https://github.com/blade47/fumadocs/blob/main/packages/story/src/vite/client.tsx)
- [packages/preview/fumadocs.config.ts](https://github.com/blade47/fumadocs/blob/main/packages/preview/fumadocs.config.ts)
- [packages/story/css/preset.css](https://github.com/blade47/fumadocs/blob/main/packages/story/css/preset.css)
- [packages/cli/src/commands/shared.ts](https://github.com/blade47/fumadocs/blob/main/packages/cli/src/commands/shared.ts)
- [packages/preview/src/cli/loader.ts](https://github.com/blade47/fumadocs/blob/main/packages/preview/src/cli/loader.ts)
</details>

The Story Visualizer is a sophisticated documentation subsystem designed to bridge the gap between static component documentation and interactive development environments. Its primary purpose is to automatically extract type information from TypeScript components at build time (or runtime, via caching) and generate an interactive control interface for those components within documentation pages.

By leveraging `ts-morph` to analyze static types, the Story Visualizer eliminates the manual overhead of writing and maintaining complex component props tables or playground controls. It generates a "Type Tree"—a structural representation of component props—which is then consumed by the client-side `WithControl` component to render an interactive editor. This system is architected for maximum automation, ensuring that changes to component interfaces are reflected in the documentation immediately upon build.

The architecture centers around a high-precision type-to-node transformation pipeline. It handles complex TypeScript features such as unions, intersections, literal types, and object properties by recursively walking the type graph and converting it into a JSON-serializable `TypeNode` format. This serialization enables the subsystem to work across various environments, from standard build-time environments (Vite/Webpack) to restricted serverless environments where full compiler access might be prohibited.

## The Type Transformation Pipeline

The core mechanism for generating story controls is the `TypeTreeBuilder`. This system uses a modular handler pattern to convert `ts-morph` `Type` objects into a custom `TypeNode` schema. The builder walks the type graph, applying specific handlers for primitive types, enums, arrays, and complex objects.

The traversal is governed by a prioritized sequence of handlers. If a handler cannot process a type, it delegates to the `next` handler.

```mermaid
flowchart TD
    A["TypeTreeBuilder.typeToNode()"] --> B["callHandler()"]
    B --> C["Check Handler Cache"]
    C -- "Cache Hit" --> D["Return cached TypeNode"]
    C -- "Cache Miss" --> E["Execute Handler"]
    E --> F{"Handler Logic"}
    F --> G["baseHandler"]
    F --> H["Custom Handlers (e.g., literalEnumHandler)"]
    G --> I["Recursively call root()"]
    I --> B
```
Sources: [packages/story/src/type-tree/builder.ts:233-282](https://github.com/blade47/fumadocs/blob/main/packages/story/src/type-tree/builder.ts#L233-L282)

> [!NOTE]
> The `cache` is implemented using `Map<TypeToNodeFlag, WeakMap<Type, TypeNode>>` to prevent memory leaks while ensuring deterministic performance when processing deep type trees. The `WeakMap` uses the `Type` object itself as a key, ensuring that nodes are garbage collected when the compiler project is disposed.

## Build-time Transformation

The `transformStoryFile` utility performs the "magic" of story generation at build time. It intercepts files matching the story pattern, scans for `defineStory` function calls, and generates the necessary metadata.

1. **Detection**: `findDefineStoryCalls` identifies all `defineStory` invocations.
2. **Analysis**: `generateControls` creates a temporary type alias in the source file, which acts as a bridge to obtain the component's props type.
3. **Serialization**: The generated `TypeNode` tree is serialized into a JSON string and injected back into the source file as an `_generated` property.

```mermaid
sequenceDiagram
    participant P as Source File
    participant T as transformStoryFile
    participant C as Compiler (ts-morph)
    participant G as generateControls

    P->>T: File Content
    T->>C: Create Virtual SourceFile
    T->>G: Extract Type Information
    G->>C: Get Type Alias
    C-->>G: TypeNode[]
    G-->>T: Serialized JSON
    T->>P: Inject _generated property
```
Sources: [packages/story/src/utils/transform.ts:25-45](https://github.com/blade47/fumadocs/blob/main/packages/story/src/utils/transform.ts#L25-L45)

## Control Injection and Interaction

Once a story is loaded in the browser, the `WithControl` component provides the interactive UI. It uses the `stf` (Story State Framework) to manage the state of component properties.

The key design choice here is the use of `useDeferredValue` for argument updates. This ensures that the documentation interface remains responsive even if the component being visualized is computationally expensive or prone to re-rendering.

| Feature | Mechanism | Benefit |
| :--- | :--- | :--- |
| **State Management** | `StfProvider` context | Decouples state from UI hierarchy |
| **Type Resolution** | `Deserialize(JSON)` | Allows runtime type enforcement |
| **Error Handling** | `ErrorBoundary` | Prevents component failures from crashing the page |
| **Updates** | `useListener` | Batching updates via `setTimeout` to limit re-renders |

Sources: [packages/story/src/client/with-control.tsx:28-81](https://github.com/blade47/fumadocs/blob/main/packages/story/src/client/with-control.tsx#L28-L81)

## Data Structure: TypeNode

The `TypeNode` is the foundational structure for story controls. It is a discriminated union that allows the `WithControl` component to dynamically render the appropriate input field for any given property type.

| Node Type | Purpose | Key Attributes |
| :--- | :--- | :--- |
| `string` | Text input | None |
| `number` | Numeric input | None |
| `boolean` | Checkbox/Switch | None |
| `literal` | Fixed choice | `value` |
| `enum` | Select dropdown | `members` (label, value) |
| `object` | FieldSet grouping | `properties` (name, type, required) |
| `union` | Polymorphic controls | `types` (array of TypeNode) |

Sources: [packages/story/src/type-tree/types.ts](https://github.com/blade47/fumadocs/blob/main/packages/story/src/type-tree/types.ts)

## Error Handling and Invariants

The Story Visualizer employs strict guards during the traversal of type definitions to avoid infinite recursions or unhandled edge cases.

> [!WARNING]
> `TypeToNodeFlag.NoIntersection` is a critical flag used to prevent the builder from descending infinitely into recursive intersections. When set, it forces the parser to treat the intersection as a terminal node.

> [!IMPORTANT]
> The `collapse` function performs a deterministic state reduction. It should be used when `fixed` values are provided by the user. If an object property is provided as a fixed value, it recursively prunes the `TypeNode` tree to remove irrelevant properties, optimizing the rendering process.

Sources: [packages/story/src/type-tree/builder.ts:5-8, 284-317](https://github.com/blade47/fumadocs/blob/main/packages/story/src/type-tree/builder.ts#L5-L8)

## Worked Example: Defining a Story

Developers can define a story with custom controls or use the default generated ones.

```typescript
import { defineStoryFactory } from '@fumadocs/story';
import { Button } from './components/button';

const { defineStory } = defineStoryFactory();

export const MyStory = defineStory('/path/to/component.tsx', {
  Component: Button,
  args: {
    initial: { variant: 'primary', children: 'Click me' },
    fixed: { disabled: true },
    controls: {
      transform: (node) => {
        // Custom logic to modify generated controls
        return node;
      }
    }
  }
});
```
Sources: [packages/story/src/index.tsx:105-157](https://github.com/blade47/fumadocs/blob/main/packages/story/src/index.tsx#L105-L157)

## Related

- [Preview Environment](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/developer-tools/preview-environment)


## Sitemap

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