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

> Explore Storybook preview-web internals. Learn how PreviewWeb, UrlStore, and WebView manage story rendering, URL state, and DOM manipulation for seamless component display.

- Repository: [Storybook/storybook](https://github.com/storybookjs/storybook)
- Tags: deep-dive
- Published: 2026-02-27

---

**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`](https://github.com/storybookjs/storybook/blob/main/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`](https://github.com/storybookjs/storybook/blob/main/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`](https://github.com/storybookjs/storybook/blob/main/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:

```typescript
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`](https://github.com/storybookjs/storybook/blob/main/UrlStore.ts)).

## The Selection and Rendering Pipeline

Once initialized, `PreviewWeb` (via its parent `PreviewWithSelection`) executes **`selectSpecifiedStory()`** (lines 26‑73 in [`PreviewWithSelection.tsx`](https://github.com/storybookjs/storybook/blob/main/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:

```typescript
// 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`](https://github.com/storybookjs/storybook/blob/main/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`](https://github.com/storybookjs/storybook/blob/main/UrlStore.ts)) parses initial URL parameters via `getSelectionSpecifierFromPath()` and maintains synchronization through `history.replaceState()` when `setSelection()` or `setQueryParams()` is called.
- **WebView** ([`WebView.ts`](https://github.com/storybookjs/storybook/blob/main/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`](https://github.com/storybookjs/storybook/blob/main/PreviewWeb.tsx) that composes the system together by constructing `UrlStore` and `WebView` instances and passing them to `PreviewWithSelection`. `PreviewWithSelection` (in [`PreviewWithSelection.tsx`](https://github.com/storybookjs/storybook/blob/main/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`](https://github.com/storybookjs/storybook/blob/main/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.