Playwright Component Testing vs Storybook Testing in 2026
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
| Capability | Playwright CT | Storybook |
|---|---|---|
| Real browser testing | Yes | Via test runner |
| Component documentation | No | Yes (primary purpose) |
| Visual regression catalog | No | Yes (entire story catalog) |
| Accessibility testing | Via axe integration | Built-in addon |
| Interactive stories | No | Yes (play functions) |
| VSCode integration | Excellent | Good |
| Trace viewer | Yes | No |
| Network mocking | Excellent | Limited |
| Team-browsable component library | No | Yes |
| Build time | Fast | Slow (full build) |
The 2026 Testing Strategy
Most teams in 2026 combine both tools:
- Storybook stories: Document every component state, use play functions for interaction tests, run the visual regression suite in CI
- Playwright CT: For complex interaction sequences (multi-step forms, drag-and-drop, keyboard navigation) that benefit from the full Playwright API and trace viewer
- Portable stories: Use
composeStoriesto 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.
See the live comparison
View playwright component vs. storybook testing on PkgPulse →