Skip to main content

Playwright Component Testing vs Storybook Testing for React Components (2026)

·PkgPulse Team
0

Playwright Component Testing runs your React components in a real browser and lets you interact with them using Playwright's full testing API. Storybook gives you isolated component development, living documentation, and then integrates Playwright for visual regression and accessibility testing. These aren't competitors — most teams use both. But understanding which tool solves which testing problem prevents duplication and tooling confusion.

TL;DR

Playwright Component Testing when you need browser-native component interaction tests with Playwright's powerful selector API and trace viewer. Storybook when component documentation, isolated development, and visual regression testing across your entire story catalog are priorities. In practice, Storybook's portable stories now work natively in Playwright CT — the combination is the 2026 standard for thorough component testing.

Key Takeaways

  • Playwright CT: Run React/Vue/Svelte components in real browsers with Playwright selectors
  • Storybook 8: Component workshop + visual testing + accessibility testing + Vitest integration
  • Storybook portable stories: Reuse stories directly in Playwright CT tests (no duplication)
  • Playwright CT: VSCode integration, trace viewer, test generator, parallel execution
  • Storybook + Playwright: Visual regression testing for the entire story catalog
  • Both: Support React, Vue, Svelte, Angular, and other frameworks
  • Key difference: CT tests are ephemeral; Storybook builds a living documentation site

Playwright Component Testing

Package: @playwright/experimental-ct-react (or vue/svelte/solid) Part of: Playwright test runner (24M+ weekly downloads) Creator: Microsoft

Playwright CT renders components directly in a real browser — not jsdom, not a virtual DOM — using Playwright's WebSocket-based browser control.

Installation

npm install -D @playwright/test @playwright/experimental-ct-react
npx playwright install chromium

Configuration

// playwright-ct.config.ts
import { defineConfig, devices } from '@playwright/experimental-ct-react';

export default defineConfig({
  testDir: './src',
  snapshotDir: './__snapshots__',
  testMatch: /.*\.spec\.(js|ts|jsx|tsx)/,
  use: {
    ctPort: 3100,
    // Use your app's build config:
    ctViteConfig: {
      resolve: {
        alias: { '@': './src' },
      },
    },
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
  ],
});

Writing Component Tests

// components/Button.spec.tsx
import { test, expect } from '@playwright/experimental-ct-react';
import { Button } from './Button';

test('renders correctly', async ({ mount }) => {
  // mount() renders the component in a real browser
  const component = await mount(<Button variant="primary">Click me</Button>);

  // Use Playwright's full selector API
  await expect(component).toHaveText('Click me');
  await expect(component).toHaveCSS('background-color', 'rgb(59, 130, 246)');
  await expect(component).toBeVisible();
});

test('handles click events', async ({ mount }) => {
  let clicked = false;

  const component = await mount(
    <Button onClick={() => { clicked = true; }}>Submit</Button>
  );

  await component.click();
  expect(clicked).toBe(true);
});

test('is accessible', async ({ mount }) => {
  const component = await mount(<Button>Submit</Button>);

  // Check ARIA attributes
  await expect(component).toHaveRole('button');
  // Check no accessibility violations (requires axe integration)
});

test('shows loading state', async ({ mount }) => {
  const component = await mount(<Button loading>Submit</Button>);

  await expect(component).toHaveAttribute('aria-disabled', 'true');
  await expect(component.getByRole('progressbar')).toBeVisible();
});

Testing Complex Interactions

// components/Dropdown.spec.tsx
import { test, expect } from '@playwright/experimental-ct-react';
import { Dropdown } from './Dropdown';

test('opens and closes on trigger click', async ({ mount, page }) => {
  const component = await mount(
    <Dropdown
      trigger={<button>Open</button>}
      items={['Option 1', 'Option 2', 'Option 3']}
    />
  );

  // Initially closed
  await expect(page.getByRole('listbox')).toBeHidden();

  // Click trigger
  await component.getByRole('button').click();

  // Now open
  await expect(page.getByRole('listbox')).toBeVisible();
  await expect(page.getByRole('option', { name: 'Option 1' })).toBeVisible();

  // Select an option
  await page.getByRole('option', { name: 'Option 2' }).click();

  // Closed after selection
  await expect(page.getByRole('listbox')).toBeHidden();
});

test('keyboard navigation works', async ({ mount, page }) => {
  const component = await mount(<Dropdown items={['A', 'B', 'C']} />);

  await component.click();
  await page.keyboard.press('ArrowDown');
  await expect(page.getByRole('option', { name: 'A' })).toHaveAttribute('aria-selected', 'true');

  await page.keyboard.press('ArrowDown');
  await expect(page.getByRole('option', { name: 'B' })).toHaveAttribute('aria-selected', 'true');
});

Playwright CT Strengths

  • Real browser: tests pass in the exact environment users see
  • Full Playwright API: powerful selectors, network mocking, file uploads
  • Trace viewer: visual timeline of every test action with screenshots
  • VSCode extension: run/debug individual tests with one click
  • Test generator: record interactions and generate test code automatically
  • Fast parallel execution across multiple browsers

Playwright CT Limitations

  • Experimental (though stable in practice)
  • No documentation website generated (tests are ephemeral)
  • No visual diff catalog — each test is a point-in-time check
  • Less opinionated about component API documentation

Storybook Testing

Package: storybook (8.x in 2026) Weekly downloads: 2.5M GitHub stars: 84K Creator: Storybook team (Chromatic-backed)

Storybook is a component workshop: you write stories (rendering states) for each component, and it builds a browsable, interactive documentation site. In Storybook 8, the testing story matured significantly with play functions, Vitest integration, and accessibility automation.

Installation

npx storybook@latest init

Writing Stories

// components/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

const meta: Meta<typeof Button> = {
  component: Button,
  title: 'UI/Button',
  argTypes: {
    variant: {
      control: 'select',
      options: ['primary', 'secondary', 'destructive'],
    },
    size: {
      control: 'select',
      options: ['sm', 'md', 'lg'],
    },
  },
};

export default meta;
type Story = StoryObj<typeof Button>;

// Each export is a story — a rendering state of the component
export const Primary: Story = {
  args: { variant: 'primary', children: 'Primary Button' },
};

export const Secondary: Story = {
  args: { variant: 'secondary', children: 'Secondary' },
};

export const Loading: Story = {
  args: { loading: true, children: 'Loading...' },
};

Play Functions: Interactive Tests in Stories

Storybook 8's play functions let you write interaction tests inside stories:

import { userEvent, within, expect } from '@storybook/test';

export const FormSubmission: Story = {
  args: { /* ... */ },
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);

    // Fill in the form
    await userEvent.type(canvas.getByLabelText('Name'), 'Alice Johnson');
    await userEvent.type(canvas.getByLabelText('Email'), 'alice@example.com');

    // Submit
    await userEvent.click(canvas.getByRole('button', { name: 'Submit' }));

    // Assert
    await expect(canvas.getByText('Success!')).toBeInTheDocument();
  },
};

These play functions run both in the Storybook UI (visual) and in the test runner (automated).

Storybook Vitest Integration (v8)

Storybook 8 integrates with Vitest for fast unit-level testing of stories:

npx storybook@latest add @storybook/experimental-addon-test
// vitest.config.ts
import { storybookTest } from '@storybook/experimental-addon-test/vitest-plugin';

export default defineConfig({
  plugins: [
    storybookTest({ configDir: '.storybook' }),
  ],
  test: {
    browser: {
      enabled: true,
      provider: 'playwright',
    },
  },
});

Now Vitest runs all your Storybook stories as tests automatically — each story with a play function becomes a test case.

Visual Regression Testing

Storybook integrates with Playwright for visual regression:

// .storybook/test-runner.ts
import { checkA11y } from 'axe-playwright';
import { toMatchImageSnapshot } from 'jest-image-snapshot';

module.exports = {
  async postVisit(page, context) {
    // Accessibility testing
    await checkA11y(page, '#storybook-root', { detailedReport: true });

    // Visual regression
    const image = await page.screenshot();
    expect(image).toMatchImageSnapshot({
      failureThreshold: 0.01,
      failureThresholdType: 'percent',
    });
  },
};

Run against your entire story catalog:

npx test-storybook --url http://localhost:6006
# Takes screenshots of every story, compares to baseline

Portable Stories: Storybook + Playwright CT

The 2026 combination: write stories in Storybook, reuse them directly in Playwright CT:

// Button.playwright.spec.tsx
import { test, expect } from '@playwright/experimental-ct-react';
import { composeStories } from '@storybook/react';
import * as ButtonStories from './Button.stories';

// Import your Storybook stories
const { Primary, Loading, Disabled } = composeStories(ButtonStories);

test('primary button renders correctly', async ({ mount }) => {
  // Mount the "Primary" story from Storybook
  const component = await mount(<Primary />);
  await expect(component).toBeVisible();
  await expect(component).toHaveText('Primary Button');
});

test('loading button shows spinner', async ({ mount }) => {
  const component = await mount(<Loading />);
  await expect(component.getByRole('progressbar')).toBeVisible();
});

One source of truth: your stories define component states, both Storybook and Playwright CT use them.

Storybook Accessibility Testing

// Storybook a11y addon — automatic accessibility checks in the UI:
npx storybook@latest add @storybook/addon-a11y

Every story gets an Accessibility tab in the Storybook UI showing WCAG violations, color contrast issues, and ARIA attribute problems — without writing any test code.

Comparison: What Each Tool Does Best

CapabilityPlaywright CTStorybook
Real browser testingYesVia test runner
Component documentationNoYes (primary purpose)
Visual regression catalogNoYes (entire story catalog)
Accessibility testingVia axe integrationBuilt-in addon
Interactive storiesNoYes (play functions)
VSCode integrationExcellentGood
Trace viewerYesNo
Network mockingExcellentLimited
Team-browsable component libraryNoYes
Build timeFastSlow (full build)

The 2026 Testing Strategy

Most teams in 2026 combine both tools:

  1. Storybook stories: Document every component state, use play functions for interaction tests, run the visual regression suite in CI
  2. Playwright CT: For complex interaction sequences (multi-step forms, drag-and-drop, keyboard navigation) that benefit from the full Playwright API and trace viewer
  3. Portable stories: Use composeStories to run Storybook stories in Playwright CT — no duplication

The two tools aren't redundant. Storybook answers "what does this component look like in all its states?" Playwright CT answers "does this complex interaction sequence work correctly in a real browser?"

Framework Support and Migration Paths

Both Playwright CT and Storybook support multiple frontend frameworks, but their support levels and maturity vary. Playwright CT has official adapters for React (@playwright/experimental-ct-react), Vue (@playwright/experimental-ct-vue), Svelte (@playwright/experimental-ct-svelte), and Solid (@playwright/experimental-ct-solid). The "experimental" label has been stable in practice since Playwright 1.35, and the API is not expected to have breaking changes before the label is removed. Framework adapters use Vite internally, which means Playwright CT benefits from Vite's fast dev server and HMR during test development — running npx playwright test --ui shows a visual test runner with live reloading as you modify component tests.

Storybook 8 supports the same framework set with additional coverage for Angular and Web Components — ecosystems where Playwright CT has no official adapter. For teams working with Angular or building framework-agnostic web components, Storybook is the only option in this comparison with first-class support. Migrating between frameworks (for example, from React class components to React hooks, or from React to Vue) can be partially validated through Storybook's visual catalog — component stories that pass in one implementation but not another quickly surface behavioral regressions. This visual migration validation is a practical use of Storybook's catalog beyond testing, making it valuable during framework migrations that extend over multiple sprints.

CI Performance and Parallelization

Test execution speed in CI is one of the most practical factors in choosing between these tools for large component libraries. Playwright CT runs components in real browser processes — each test worker spawns a browser, loads the component in isolation, executes interactions, and reports results. Playwright's parallelization model runs multiple workers (typically one per CPU core in CI) simultaneously, and tests within each worker run sequentially. For a library with 200 component tests, Playwright CT on a 4-core CI runner completes in roughly 30–60 seconds depending on component complexity. The trace viewer artifacts generated on failure add meaningful debugging value but also increase CI storage requirements.

Storybook's test runner, which executes play functions via Playwright for visual regression, runs each story serially by default — running the full story catalog including visual regression snapshots for 200 stories can take 5–15 minutes depending on the snapshot comparison approach. Chromatic (Storybook's cloud visual regression service) parallelizes this dramatically using its distributed browser fleet, reducing visual regression CI time to 30–60 seconds for the same catalog, but Chromatic adds per-snapshot cost at scale. Teams using local visual regression (via jest-image-snapshot) keep costs low but accept longer CI times. For organizations with strict CI budget constraints, Playwright CT's component-test-only approach without visual snapshots is typically the fastest path to reliable component coverage.

Component Documentation as a Living Artifact

One dimension where Storybook's value extends beyond testing is its role as living documentation for design systems. A Storybook deployed to a static hosting URL — with storybook build producing a self-contained static site — gives designers, product managers, and stakeholders a browsable reference for every component state without requiring access to the codebase. This documentation function survives long after the original developer who built the component has left the team. Playwright CT, by contrast, produces test results that live in CI artifacts and are not naturally shareable with non-technical stakeholders.

For design systems maintained by a dedicated team and consumed by multiple product teams, this documentation artifact is often the primary justification for Storybook's setup and maintenance cost. The @storybook/addon-docs addon generates a documentation site from story metadata, prop types, and JSDoc comments — when properly maintained, it provides a component library reference comparable to commercial design system documentation tools like ZeroHeight or Supernova, without the additional cost. Teams that have invested in Storybook documentation and find Playwright CT's interaction testing coverage complementary can adopt both tools through the portable stories pattern, using each tool for its primary strength.

Compare testing library downloads on PkgPulse.

Compare Playwright-component and Storybook-testing package health on PkgPulse.

See also: React vs Vue and React vs Svelte, Playwright Component Testing vs Storybook Testing.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.