Documentation
Workbench docsGetting started

Pages and states

A design page is an HTML file in your repository. This guide covers how those pages are served, how one page shows several states, how links behave, and how to choose the artboard sizes a page supports. To render components from your codebase instead, see TypeScript previews. To document components in Markdown with live examples, see Docs.

Design pages

Any HTML file in the project can be a page: a hand-written page, a design system’s component preview, or a static export. List it in workbench.yaml:

collections:
  - name: Pages
    items:
      - label: Dashboard
        src: design/dashboard.html

Pages need nothing added for Workbench. No script tag, import, or build step.

How pages are served

The extension runs a small server on 127.0.0.1 that serves your project at / and the workbench at /_workbench/, on one origin. That has a few consequences for how you write pages:

  • Paths behave as on a normal web server rooted at the project. A relative path (../styles/app.css) resolves from the page. A root-relative path (/styles/app.css) resolves from the project root.
  • One script is added to every HTML page the server serves, and to TypeScript previews. /_workbench/preview-compat.js goes first in <head> and handles states, actions, and forwarding editor shortcuts to VS Code. You don’t add it yourself.
  • A page opened straight from disk gets no script. It shows as authored, and its links behave like ordinary links.

Because the canvas and your page share an origin, the workbench can read the live page. That is what lets it name the element under each annotation and take a screenshot of exactly what you see.

What works in a page

Pages are real pages in a Chromium-based browser: scripts, web components, shadow DOM, web fonts, canvas, video, dialogs, and forms all work. The screenshot path copies the live document, so closed shadow roots don’t appear in screenshots, and a page containing an iframe, object, or embed, or a canvas or video loaded from another origin without CORS, can’t be captured. See Screenshot limits.

States

A state is a variation of the same page: the empty form and the form that came back with an error, a list with rows and the same list with none. The page list shows a page’s states under it, the way Storybook lists stories under a component.

A state is declared twice, and the ids must match:

  1. In workbench.yaml, which adds the state to the page list.
  2. In the page, which decides what the state looks like.
- label: Sign in
  src: pages/sign-in.html
  states:
    - id: default
      label: Default
    - id: error
      label: Wrong password
    - id: sending
      label: Signing in

When you pick a state, the page loads with ?state=<id>. The first state is the page as authored and loads with no parameter. Workbench’s script reads the parameter and sets data-wb-state on <html>: default for the first state, or the id. An id that isn’t kebab-case is treated as default.

The first state always appears in the page as default, whatever its id. Give it the id default so the YAML and the page agree. If it has another id, such as primary, refer to it as default in the page.

A page can answer in three ways. Use the smallest one that works.

1. CSS keyed off the root

.alert { display: none; }
html[data-wb-state="error"] .alert { display: block; }
html[data-wb-state="error"] .field input { border-color: var(--red); }

This suits differences that are purely visual.

2. Markup that exists only in some states

<p class="alert" data-wb-state-only="error">That password isn't right.</p>
<p class="hint" data-wb-state-not="error sending">Use your work email.</p>
  • data-wb-state-only="a b" keeps the element in states a and b, and removes it in every other state.
  • data-wb-state-not="a b" removes the element in states a and b.

Ids are space-separated. Elements are removed, not hidden, so they don’t take up space, affect layout, or appear in a screenshot. To keep an element in the default state, include default in the list.

3. Attributes set in one state

<input type="email" data-wb-set-error="value=dana@example.com; aria-invalid=true">
<button data-wb-set-sending="disabled; aria-busy=true">Sign in</button>

data-wb-set-<id> sets attributes on that element in state <id>. Separate pairs with semicolons. Write name=value, or a bare name for a boolean attribute. Only the active state’s attribute is read; the others are left in the markup as a record of what the element does elsewhere.

When states are applied

The script sets the root attribute immediately, then applies only, not, and set at DOMContentLoaded. Custom elements loaded with defer are already upgraded by then, so they receive the attributes normally. Elements that your scripts add later aren’t processed, so key late content off the root attribute with CSS, or read document.documentElement.dataset.wbState in your script:

if (document.documentElement.dataset.wbState === 'empty') renderEmptyState();

Tips

  • Keep ids short and descriptive. They appear in links and screenshot names: sign-in-error.jpg.
  • A page needs at least two states to show any in the page list. One state is the same as none.
  • When a state needs data the page doesn’t have, render it from a small inline fixture keyed by dataset.wbState, rather than making a second page.
  • State ids also map to implementation paths, so the same state can be shown on your dev server. See Map states to paths.
  • In a docs lens, the page’s state applies to every example in its docs. A Markdown page’s states aren’t listed in the page list.

The top bar’s Actions switch decides whether a page’s links and forms work.

  • Off (the default): links don’t navigate and forms don’t submit. Hover, focus, pressed states, disclosure widgets, pickers, and anything else that only changes the page in place still work. You can click around a page you’re reviewing without leaving it.
  • On: the page behaves normally, so you can walk through a real flow.

With actions on, a link to another page listed in workbench.yaml switches the workbench to that page, and the sidebar follows. A ?state=<id> on the link picks that state, as in <a href="sign-in.html?state=error">. A form whose action names a listed page does the same when it is submitted, so a flow can move from one page to the next.

A link or form to an .html file in the project that isn’t listed as a page does nothing. List the page in workbench.yaml to make it reachable. Links to other sites, to project paths that don’t end in .html, mailto: links, and downloads behave as usual.

A link to a spot on the same page, such as href="#pricing", scrolls to it whether actions are on or off.

Workbench previews are mocks rather than pages: with actions on, their links open the previews their definition maps them to, and any other link or form is recorded under Actions instead of followed.

Either way, browser form validation is off, so a required field doesn’t stop a flow.

The switch applies to pages Workbench serves. Implementations shown through a lens are other sites, and their links always work.

Workbench sets data-wb-actions="on" or "off" on <html> if your styles need to know.

Sizes

sizes lists the artboard sizes a page is designed for. The size switcher’s other sizes are disabled while it is showing, and exports capture one reference image per size.

- label: Reset password
  src: pages/reset.html
  sizes:
    - mobile
    - resizable
Size Artboard Use it for
laptop 1512 × 982, a 14-inch MacBook Pro Laptop and desktop layouts
mobile 393 × 852, an iPhone 15 Pro Phone layouts
resizable An artboard you resize by dragging its edges or corners Layouts that should work at any width. Exports both laptop and mobile references.
fit Fills the canvas Components and pages without a fixed device size. Exports at 1440 × 900.

Omit sizes to allow all four. The page always lays out at the artboard’s real size; when the artboard is larger than the canvas, the canvas zooms out rather than squeezing the page. See Artboard sizes and zoom. In a docs lens, the docs fill the canvas and the size switcher is disabled; the design lens keeps the page’s size. A Markdown page has no artboard and takes no sizes.

Component previews

For a design system, a common pattern is one preview page per component, with its variants as states:

- name: Components
  icon: component
  items:
    - label: Button
      src: components/button.html
      sizes:
        - fit
      states:
        - id: default
          label: Primary
        - id: secondary
          label: Secondary
        - id: disabled
          label: Disabled
<!-- components/button.html -->
<button class="btn"
        data-wb-set-secondary="class=btn btn-secondary"
        data-wb-set-disabled="disabled">Continue</button>

To render the real components from your codebase instead of HTML copies, write TypeScript previews. To document each component on one page, with its examples, their code, and props tables, give the page docs. If your components already have Storybook stories, consider importing them.