Playwright Component Testing vs Storybook Testing: Component Tests 2026
·PkgPulse Team
TL;DR
Use Playwright component testing for fast, isolated component tests that run in real browsers. Use Storybook for visual development and design system documentation — and add Storybook test runner as a bonus. They serve different primary purposes: Playwright CT is a testing tool that can show visuals; Storybook is a development/documentation tool that can run tests. In 2026, many teams use both: Storybook for development + Playwright CT for CI testing.
Key Takeaways
- Playwright CT: Real browser testing, 3x faster than Cypress CT, integrates with existing Playwright setup
- Storybook: Visual development sandbox, test runner via
@storybook/test-runner, interaction tests - Speed: Playwright CT (parallel, real browsers) vs Storybook runner (serial by default)
- Coverage: Playwright CT has better code coverage support; Storybook adds visual regression
- DX: Storybook has better visual feedback; Playwright CT has better test authoring
- 2026 choice: Playwright CT for teams that already use Playwright; Storybook if you need visual docs
Downloads
| Package | Weekly Downloads | Trend |
|---|---|---|
@playwright/test | ~5M | ↑ Growing |
@storybook/react | ~3M | → Stable |
@storybook/test-runner | ~500K | ↑ Growing |
Playwright Component Testing
npm install -D @playwright/experimental-ct-react
# Or for other frameworks:
npm install -D @playwright/experimental-ct-vue
npm install -D @playwright/experimental-ct-svelte
// playwright-ct.config.ts:
import { defineConfig, devices } from '@playwright/experimental-ct-react';
export default defineConfig({
testDir: './src',
testMatch: '**/*.ct.{ts,tsx}',
use: {
ctPort: 3100,
ctViteConfig: {
// Your Vite config here
},
},
projects: [
{ name: 'chromium', use: devices['Desktop Chrome'] },
{ name: 'firefox', use: devices['Desktop Firefox'] },
],
});
// Button.ct.tsx — Playwright component test:
import { test, expect } from '@playwright/experimental-ct-react';
import { Button } from './Button';
test('renders with correct text', async ({ mount }) => {
const component = await mount(<Button variant="default">Click Me</Button>);
await expect(component).toContainText('Click Me');
await expect(component).toHaveClass(/bg-primary/);
});
test('calls onClick when clicked', async ({ mount }) => {
let clicked = false;
const component = await mount(
<Button onClick={() => { clicked = true; }}>Click Me</Button>
);
await component.click();
expect(clicked).toBe(true);
});
test('disabled button is not clickable', async ({ mount }) => {
const component = await mount(<Button disabled>Disabled</Button>);
await expect(component).toBeDisabled();
await expect(component).toHaveAttribute('disabled');
});
test('visual snapshot', async ({ mount, page }) => {
const component = await mount(
<div className="p-4 bg-white">
<Button variant="default">Default</Button>
<Button variant="outline">Outline</Button>
<Button variant="destructive">Delete</Button>
</div>
);
await expect(component).toHaveScreenshot('buttons.png');
});
// Testing with context providers:
test('modal with auth context', async ({ mount }) => {
const component = await mount(
<AuthProvider user={{ id: '1', name: 'John' }}>
<DeleteConfirmDialog onDelete={() => {}} />
</AuthProvider>
);
// Trigger the dialog:
await component.getByRole('button', { name: 'Delete' }).click();
// Verify dialog appeared:
await expect(component.getByRole('dialog')).toBeVisible();
await expect(component.getByText('Are you sure?')).toBeVisible();
});
Storybook: Visual Development + Testing
npx storybook@latest init # Detects framework automatically
npm install -D @storybook/test-runner
// Button.stories.tsx — story definition:
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
import { expect, fn, within } from '@storybook/test';
const meta: Meta<typeof Button> = {
title: 'UI/Button',
component: Button,
tags: ['autodocs'],
argTypes: {
variant: { control: 'select', options: ['default', 'outline', 'destructive', 'ghost'] },
size: { control: 'select', options: ['default', 'sm', 'lg', 'icon'] },
},
};
export default meta;
type Story = StoryObj<typeof Button>;
// Basic stories (visual documentation):
export const Default: Story = {
args: { children: 'Button', variant: 'default' },
};
export const Outline: Story = {
args: { children: 'Outline', variant: 'outline' },
};
export const Destructive: Story = {
args: { children: 'Delete', variant: 'destructive' },
};
// Interaction test (runs in test runner AND browser):
export const ClickTest: Story = {
args: {
children: 'Click Me',
onClick: fn(),
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const button = canvas.getByRole('button', { name: 'Click Me' });
await button.click();
await expect(args.onClick).toHaveBeenCalledOnce();
await expect(button).toHaveFocus();
},
};
// Accessibility test:
export const AccessibilityTest: Story = {
args: { children: 'Accessible Button' },
play: async ({ canvasElement }) => {
// Storybook's a11y addon runs checks automatically
// Additional manual checks:
const canvas = within(canvasElement);
const button = canvas.getByRole('button');
await expect(button).toBeInTheDocument();
},
};
# Run Storybook test runner:
npx storybook@latest dev -p 6006 # Start Storybook
npx test-storybook # Run all play functions as tests
# Or in CI (headless):
npx concurrently -k -s first -n "SB,TEST" \
"npx storybook dev --ci" \
"npx wait-on tcp:6006 && npx test-storybook"
Speed Comparison
Component count: 50 components, 3 stories/tests each
Playwright CT:
→ Parallel execution (4 workers)
→ Total: 45s
→ Per component: ~300ms average
Storybook test runner:
→ Sequential by default
→ Total: 2m 30s
→ Per component: ~1s average
Playwright CT with visual snapshots:
→ Total: 1m 30s (snapshot generation is slower)
Storybook with Chromatic (cloud):
→ Parallel cloud runners
→ Total: ~30s (but paid, network dependent)
Feature Comparison
| Feature | Playwright CT | Storybook Testing |
|---|---|---|
| Real browser | ✅ Chrome/FF/Safari | ✅ Via test runner |
| Visual snapshots | ✅ toHaveScreenshot | ✅ Chromatic |
| Interaction tests | ✅ | ✅ play() functions |
| Visual development | ❌ | ✅ Primary feature |
| Design system docs | ❌ | ✅ Autodocs |
| A11y testing | Via axe-playwright | ✅ @storybook/addon-a11y |
| Code coverage | ✅ Native | Limited |
| CI speed | Fast (parallel) | Slower (sequential) |
| Chromatic integration | ❌ | ✅ First-class |
| Setup complexity | Low | Medium |
Decision Guide
Use Playwright Component Testing if:
→ Team already uses Playwright for E2E
→ Want fast isolated component tests in CI
→ Don't need visual development sandbox
→ Code coverage is important
Use Storybook if:
→ Building a design system or component library
→ Designers need to interact with components
→ Need documentation for component API
→ Want visual regression with Chromatic
Use both (recommended for teams):
→ Storybook for development + documentation
→ Playwright CT for isolated unit tests in CI
→ Storybook test runner for integration tests
→ Saves writing tests twice for documented components
Skip component testing entirely if:
→ Small project, E2E tests cover enough
→ Solo developer, unit tests + E2E sufficient
→ Moving fast, will add later
Compare Playwright and Storybook download trends on PkgPulse.