How Storybook Preview-Web Works: Deep Dive into PreviewWeb, UrlStore, and the Rendering Runtime

Storybook's preview-web module orchestrates story rendering through three coordinated classes—PreviewWeb (the composition root), UrlStore (URL state management), and WebView (DOM manipulation)—which together parse query parameters, synchronize browser history, and mount components into the iframe canvas.

The preview-web subsystem in the storybookjs/storybook repository powers the iframe runtime that renders stories and documentation. Located in code/core/src/preview-api/modules/preview-web/, this module bridges the manager UI with the actual component rendering, using the URL as the single source of truth for selection state.

Core Components of the Preview-Web Architecture

PreviewWeb: The Composition Root

The PreviewWeb class defined in PreviewWeb.tsx serves as the public entry point for the preview runtime. Its constructor (lines 11‑17) creates a PreviewWithSelection instance while wiring together the UrlStore for state persistence and WebView for DOM operations. The constructor stores the instance on global.__STORYBOOK_PREVIEW__, enabling the manager window to communicate with the preview via the channel API.

UrlStore: Synchronizing State with the Address Bar

UrlStore (UrlStore.ts) maintains bidirectional synchronization between the application state and the browser URL. During construction (lines 69‑91), it calls getSelectionSpecifierFromPath() to parse document.location.search using picoquery, extracting path, id, viewMode, args, and globals. The pathToId() helper (lines 12‑18) converts paths like /story/button--primary into canonical story IDs.

When users select a new story, setSelection() (lines 103‑106) generates a fresh query string via getQueryString() (lines 20‑35) and calls history.replaceState() (line 44) to update the URL without triggering a page reload. Additional parameters are managed through setQueryParams() (lines 108‑112), which merges new values into the existing query string.

WebView: Managing the DOM Canvas

WebView (WebView.ts) implements the View interface to manipulate the preview iframe's DOM. Key responsibilities include:

  • Layout application: Applying CSS classes via applyLayout() based on the story's layout parameter
  • Canvas preparation: prepareForStory() (lines 69‑78) shows the story canvas and returns the root element for mounting
  • Docs preparation: prepareForDocs() (lines 84‑92) switches to the docs pane with fullscreen layout
  • Loading states: showPreparingStory and showPreparingDocs toggle visibility classes after a 100ms debounce (PREPARING_DELAY)

Initialization and URL Parsing Flow

When Storybook boots, the manager creates a new PreviewWeb instance:

import { PreviewWeb } from '@storybook/preview-api';

const importFn = (path: string) => import(path);
const getProjectAnnotations = async () => ({ /* globals, decorators */ });

const preview = new PreviewWeb(importFn, getProjectAnnotations);

The constructor initializes the UrlStore, which immediately parses the current query string. If the URL contains ?path=/story/button--primary, the store converts this to a selectionSpecifier object containing the story ID and view mode (stored on line 95 of UrlStore.ts).

The Selection and Rendering Pipeline

Once initialized, PreviewWeb (via its parent PreviewWithSelection) executes selectSpecifiedStory() (lines 26‑73 in PreviewWithSelection.tsx):

  1. Retrieves the current selection from UrlStore
  2. Looks up the story entry in the index
  3. Calls this.selectionStore.setSelection() (line 66) to update state
  4. Invokes renderSelection() to mount the component

The renderSelection() method (lines 17‑42) instantiates the appropriate renderer:

  • StoryRender for component stories
  • CsfDocsRender or MdxDocsRender for documentation pages

Before rendering, this.view.prepareForStory(story) (line 71) configures the DOM environment, applying layout classes and returning the target element from storyRoot().

Programmatic URL Manipulation

You can trigger URL updates from within the preview via channel events:

// Change current story
preview.channel.emit('SET_CURRENT_STORY', {
  storyId: 'button--secondary',
  viewMode: 'story'
});
// URL updates to: ?id=button--secondary&viewMode=story

// Add custom query parameters
preview.channel.emit('UPDATE_QUERY_PARAMS', { 
  nightMode: true 
});
// URL updates to: ?id=button--primary&viewMode=story&nightMode=true

These events are handled by PreviewWithSelection.onSetCurrentStory and UrlStore.setQueryParams respectively, ensuring the address bar always reflects the current state without page reloads.

Summary

  • PreviewWeb (PreviewWeb.tsx) acts as the composition root, instantiating PreviewWithSelection, UrlStore, and WebView, and exposing the preview instance globally via global.__STORYBOOK_PREVIEW__.
  • UrlStore (UrlStore.ts) parses initial URL parameters via getSelectionSpecifierFromPath() and maintains synchronization through history.replaceState() when setSelection() or setQueryParams() is called.
  • WebView (WebView.ts) implements the View interface to manage DOM elements, applying layouts and preparing the canvas via prepareForStory() and prepareForDocs().
  • The rendering pipeline in PreviewWithSelection coordinates these components to load stories based on URL state, enabling shareable, bookmarkable links that restore exact story configurations including args and globals.

Frequently Asked Questions

What is the difference between PreviewWeb and PreviewWithSelection?

PreviewWeb is a thin concrete class defined in PreviewWeb.tsx that composes the system together by constructing UrlStore and WebView instances and passing them to PreviewWithSelection. PreviewWithSelection (in PreviewWithSelection.tsx) contains the core logic for handling selection events, managing the render lifecycle, and coordinating between URL state and component rendering. While PreviewWeb is the public API used by the manager, PreviewWithSelection handles the internal SET_CURRENT_STORY events and delegates to UrlStore for persistence.

How does Storybook maintain the current story selection across page reloads?

The UrlStore class treats the URL as the single source of truth. During initialization, it parses document.location.search to extract the story ID, view mode, and arguments. When users select different stories or change controls, UrlStore.setSelection() calls history.replaceState() to update the query string. On reload, the constructor re-parses these parameters via getSelectionSpecifierFromPath(), ensuring the preview resumes at the exact same state.

Can I access or modify the URL state from within my Storybook stories?

While stories themselves shouldn't manipulate the URL directly, you can access the preview's channel from decorators or addons to emit SET_CURRENT_STORY or UPDATE_QUERY_PARAMS events. These are handled by the PreviewWithSelection class, which delegates to UrlStore for URL manipulation using getQueryString() and history.replaceState().

How does WebView handle different layout modes like fullscreen or padded?

The WebView class in WebView.ts implements applyLayout() to apply CSS classes to the document body based on the story's layout parameter. When prepareForStory() is called, it clears previous layout classes and applies the new configuration (such as fullscreen or padded) before returning the root element where the story mounts.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →