Skip to main content

Playwright Component Testing vs Storybook Testing in 2026

·PkgPulse Team

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?"

Compare testing library downloads on PkgPulse.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.