Evaluating CodeRabbit? Same review, plus real test runs. See why

Automation Testing11 min read

Playwright Component Testing: Setup, Examples, and Limits

S
Technical Writer, Qodex
Playwright logo mark with the words Component testing under it
Part of our UI Testing guide. Read the guide

Playwright component testing renders one component story through your app's development server, then tests it in a real browser with Playwright locators and assertions. Since Playwright 1.62, the story-gallery workflow in @playwright/test replaces the older experimental React and Vue packages. Use it for isolated browser behavior, not complete deployed journeys.

If you want your component and end-to-end tests written for you, Qodex generates Playwright tests from a description and runs them on every pull request.

SituationPathWhat you do
Starting nowCurrent story galleryPlain @playwright/test, a gallery under playwright/gallery/, named stories mounted by ID
Existing experimental suiteMigrateSwap the framework package for @playwright/test, turn per-test mounting into stories
Reading an older tutorialLegacy referenceTreat --ct, playwright/index.html and hooksConfig as v1.55-era material
Choosing another toolCompare the runtimeCypress for direct mounting, Vitest Browser Mode inside Vitest, Testing Library for DOM queries

Playwright component testing, and where the experimental label comes from

A component test in Playwright is an ordinary Playwright test pointed at a small page. That page is the gallery. Your own development server serves it, and it renders one named story into a #root element. The test code runs in Node.js while the component runs in a real browser, so real clicks fire and real layout is computed.

The label question keeps coming up because most search results still answer it the old way. Playwright did ship this feature as experimental, and the packages said so in their names: @playwright/experimental-ct-react, -vue and -svelte. Playwright's own Testing Library migration page still carries the phrase "Experimental Component Testing" and still imports the deprecated package, which is why the confusion survives on the official site.

Playwright 1.62 replaced that architecture. The current component page calls the story gallery stable: tests import from plain @playwright/test, and mount is a documented built-in fixture. The newest release is v1.62.1, published on 30 July 2026. Two requests are still open, one for type safety and one for a command-line scaffold.

Component tests versus end-to-end tests

Both layers belong in a suite, and both run in a real browser. This page is part of our UI testing guide, which covers the layer above: complete user journeys through the deployed application.

Component testEnd-to-end test
ScopeOne story, one componentA connected journey across pages
EnvironmentYour dev server, serving the galleryThe deployed application
DependenciesOnly what the story providesReal routing, APIs, auth, data
BrowserReal events, real layoutSame
Best failure signalThis component mishandles this stateThis journey is broken for users

The common mistake is calling component testing simulated DOM testing. It is not. The component runs in a real browser while the test process stays in Node.js. What changes is the surrounding application, not the browser.

The docs list what the real browser buys you: real layout and real browser events, locator auto-waiting, screenshots, parallel runs, retries and traces. That is the same machinery your end-to-end specs already use, which is most of the argument for keeping both layers in one runner.

A checkout button can render its disabled state, react to a click and show the mocked error correctly in the gallery. The deployed checkout can still fail, because the route is wrong or the orders API rejects the request. The gallery sees neither. The layers answer different questions.

How the story gallery works, and how to set it up

Three pieces make up the whole model.

  • A story is a small wrapper component that puts the component under test into one scenario: fixed props, mock data, providers, recorded callbacks. Stories sit next to the component in *.story.* files, and each named export is one story.

  • The gallery is a single page under playwright/gallery/, served by your own development server. It discovers your story files, renders one into a #root element, and exposes window.mount() and window.unmount(). It is framework-specific and you own it.

  • The mount fixture navigates to the gallery, calls window.mount() with a story ID and optional props, and hands back a locator for the rendered root.

A story ID comes from the file path: the path under src/, without the .story.* extension, plus the export name. So src/components/Button.story.tsx exporting Primary is components/Button/Primary.

The process boundary explains most of the rules that follow. Test code is in Node.js and the component is in the browser. Everything the component needs is set up inside the story; everything the test asserts is observable through the page. Global CSS and themes belong in the gallery's window.mount body, while per-story providers, routers, mock data and callbacks belong in the story or a decorator.

Setup starts from an app that already has Playwright installed. The gallery is application code, so Playwright ships the methodology as an agent skill rather than the page itself:

npx playwright init-skills

Then ask your coding agent to set up component testing using the playwright-component-testing skill, which creates the framework-specific gallery code. That is the official quick start. There is no deterministic scaffold command yet, and the request for a plain command-line template is still open.

The config is the same either way. Add a components project and point both its baseURL and the webServer.url at the gallery page, because mount navigates to baseURL. Set serviceWorkers: 'block' so the app's service worker cannot shadow page.route() mocks with cached responses, and reuseContext: true to reuse the browser context between tests in a worker.

FrameworkWhat the docs give you
ReactAn official worked example, gallery and story
VueAn official worked example, gallery and story
SvelteNamed as supported by the framework-agnostic contract

The story ID rule, the mount contract and the config keys are identical in all three; only the gallery's rendering code differs. The docs say the same about Solid and anything else: if your development server can render it, Playwright can test it.

Run a React component test with props and a click

Here is a complete counter example in three files: the component, a story, and the spec.

// src/components/Counter.tsx
import { useState } from 'react';

export function Counter({ start = 0 }: { start?: number }) {
  const [count, setCount] = useState(start);

  return (
    <section>
      <p>Count: {count}</p>
      <button onClick={() => setCount(value => value + 1)}>Increment</button>
    </section>
  );
}
// src/components/Counter.story.tsx
import { Counter } from './Counter';

export const Default = ({ start = 0 }: { start?: number }) => <Counter start={start} />;
// tests/components/counter.spec.ts
import { test, expect } from '@playwright/test';

test('increments from the starting count', async ({ mount }) => {
  const component = await mount('components/Counter/Default', { start: 5 });

  await expect(component.getByText('Count: 5')).toBeVisible();
  await component.getByRole('button', { name: 'Increment' }).click();
  await expect(component.getByText('Count: 6')).toBeVisible();
});
npx playwright test --project=components

Read the chain from the test outwards. Default is the named export, so the story ID is components/Counter/Default, by the path rule above. The second argument to mount() is a plain props object; the gallery hands it to the story, which forwards it to the component. Callbacks belong inside the story, not in that object.

What comes back is a locator for the rendered root, not a component handle. That is why the assertions read like any other Playwright test. getByRole('button', { name: 'Increment' }) finds the button by its accessible role and name, and .click() fires a real browser click. Both toBeVisible() checks are retrying Playwright assertions, so they re-query the DOM until the condition holds or the timeout expires.

Patterns for second states, events, network errors and screenshots

Four patterns cover most of a component suite, each with a boundary worth stating.

ConcernPatternBoundary
A second stateDifferent props with the story ID, or update(newProps) without remountingupdate() passes new props to the story already mounted
EventsTrigger real input through the locator; keep callbacks in the story, record the result into a hidden inputAssert on DOM, URL or network, never on a marshalled callback
Network errorsRegister page.route() before mount(), since mounting navigatesA mock proves the component's response, not the real backend
Visual statesScreenshot the returned root locator for one deliberate stateScreenshot the root, not the page, so nothing else in the gallery is asserted on

The recorded-state pattern is worth learning first. Because the story owns the callback, it writes the observed value into a hidden form input next to the component, and the test asserts on that input with a web-first matcher. That input is readable in the element inspector when you open the story in the gallery by hand, so the story doubles as a manual test page. For the screenshot side, see visual regression testing.

The legacy experimental setup, and how to migrate

Everything in the left column is v1.55-era material. It is here so you can recognize it in an older tutorial and move off it, not so you can build with it.

v1.55 experimental packagesCurrent story gallery
npm init playwright@latest -- --ctnpx playwright init-skills, then the component-testing skill
@playwright/experimental-ct-react, -vue, -sveltePlain @playwright/test
playwright/index.html plus its adjacent index scriptThe gallery under playwright/gallery/
mount(<Component />), or mounting a Vue or Svelte component objectmount('story/id'), one named story per composition
beforeMount, afterMount, hooksConfigThe gallery's window.mount body, stories, or decorators
ctViteConfigGone: the gallery runs on your own dev server

The old command also created a separate component-test config and a test-ct script, so legacy suites ran with npm run test-ct rather than a project flag. That is the fastest way to date a tutorial.

Two more details give it away. playwright/index.html had to contain a #root element and load the script sitting next to it, and that script was where global CSS, a theme or any runtime setup went. Hooks came from the framework package's /hooks entry: beforeMount and afterMount, with hooksConfig carrying per-test router or provider settings from mount() into them.

Migrate incrementally. Stand up the gallery and the components project while the old suite keeps running, port one spec at a time, then drop the experimental dependency along with playwright/index.html. The hard part is not the imports. It is that a test which built a different JSX tree per case now needs one named story per composition.

Playwright versus Cypress, Vitest Browser Mode and Testing Library

This is a workflow comparison read from each tool's own documentation. It is not a ranking and there is no benchmark behind it.

ChoiceRuntime and modelCoverageBest fitMain tradeoff
PlaywrightA story renders through your own dev server in a real browser, with Playwright locators, retries and tracesFramework-agnostic contract; docs name React, Vue, Svelte, SolidTeams already on Playwright, sharing infrastructure across both layersYou own the gallery, story IDs are strings, setup leans on an agent skill
CypressDirect mounting in a real browser inside the Cypress App, with Time Travel, spies and clock controlMounting libraries for React, Angular, Vue, Svelte on a published bundler listTeams wanting an integrated visual runnerSupport follows a compatibility matrix, not an app-owned contract
Vitest Browser ModeVitest tests run natively in a browser through a providerRender packages for Vue, Svelte, React, AngularVite teams wanting browser tests beside unit testsCI needs a Playwright or WebdriverIO provider; no blocking dialogs
Testing LibraryDOM-first queries; runner, renderer and environment are separate choicesFramework packages for React, Vue, Svelte and moreSmall DOM checks on an existing Jest or Vitest setupNo browser projects, traces, network control or runner

Pick Playwright when your team already runs Playwright and wants component and end-to-end checks to share locators, assertions, traces and browser projects.

Pick Cypress when direct mounting and the Cypress App decide it: Time Travel through each step, spies, stubs and clock control in one place. Its support is published as a matrix that lists the Vite, webpack and Next.js combinations it covers, and Svelte 5 support is listed as alpha, so check your combination first. Our runner-level comparison covers the wider choice.

Pick Vitest Browser Mode when a Vite team wants browser component tests next to fast unit tests in one setup. Install a Playwright or WebdriverIO provider for CI, because the preview provider simulates events rather than driving the browser. Two limits come with it: native blocking dialogs such as alert are out, and it cannot spy on imported module exports the way a Node test can.

Pick Testing Library when DOM-focused behavior and your existing Jest or Vitest setup are all you need. The Testing Library decision covers where that stops being enough.

Current limits and failure modes

Separate the rough edges of the current model from the limits fixed by replacing the old one. Getting this backwards is the most common error in articles on the topic.

Current rough edges. Story IDs are plain strings, so renaming a story breaks specs at runtime rather than compile time. Generic prop typing is not tied automatically to the chosen ID either, and the type-safety request is open. Per-test JSX is gone, so every composition worth testing needs a named story. Tests cannot call a component's methods or reach its instance, which the docs list as neither recommended nor supported. And the quick start depends on a coding-agent skill, because the request for a command-line scaffold is open.

Legacy only. The experimental packages could pass only plain objects and built-in types across the Node-to-browser boundary, so complex live objects lost their methods. A user report from v1.31.1 shows exactly that, and it is closed, so treat it as migration history, not a gallery bug. Teams also mirrored aliases, plugins and CSS into ctViteConfig by hand, webpack and Next.js could not use their own build, and Node-side module mocks did not apply reliably.

From component confidence to pull request evidence

A component test can show that one story renders, reacts to input and handles a mocked failure. It cannot show that the deployed preview reaches the right route, calls the right API, or completes the journey. That gap is where a pull request check earns its place.

Qodex starts from the deployed app rather than from a story. A deterministic crawl walks the pages, and the coverage map marks each one tested, untested or failing. That is the question a gallery cannot answer: a story only exists for a state somebody already thought to write down. You describe the flow you want in a sentence, the agent drives the real app, and the run is saved as standard Playwright, parameterized per environment and synced to git. Every pull request runs the suite against its own preview, and a preview that did not boot is classified as an environment issue rather than counted as a failure.

See how Qodex runs UI tests on each pull request.

Keep component tests on the browser behavior of one story, and keep end-to-end coverage for the deployed journey, the integrations and the permissions a gallery cannot prove.

Frequently Asked Questions

Is Playwright component testing still experimental?

It was. The v1.55 packages were named @playwright/experimental-ct-react, -vue and -svelte, and Playwright's Testing Library migration page still shows that label. Playwright 1.62 replaced them with the story gallery in plain @playwright/test, which the current docs call stable, with type-safety and setup requests open.

Which frameworks does Playwright component testing support?

The official page names React, Vue, Svelte and Solid, with worked examples for React and Vue. It then calls the approach framework-agnostic: any framework works when its development server can render the gallery contract, since the gallery page is the only framework-specific piece.

How do I set up Playwright component testing for React, Vue or Svelte?

Start from an app that already has Playwright. Run npx playwright init-skills, then ask your coding agent to set up component testing with the playwright-component-testing skill. Add a components project whose baseURL and webServer.url point at the gallery.

What is the difference between Playwright component testing and end-to-end testing?

Both run in a real browser, so neither is simulated DOM testing. A component test mounts one named story in a small gallery served by your dev server. An end-to-end test covers a connected journey through the deployed application, with real routing, APIs and data.

How do I pass props to a mounted component?

mount() takes the story ID and an optional plain props object, which the gallery hands to the story. Leave callbacks inside the story. Call update(newProps) when the same mounted component needs a second state without remounting.

Is Playwright or Cypress better for component testing?

Neither wins outright. Choose Playwright when your team already shares Playwright infrastructure across test layers. Choose Cypress when direct mounting and its visual debugging matter more. This is a choice about the runner you maintain, not a performance verdict.

How does Playwright component testing compare with Vitest Browser Mode?

Playwright uses its own projects, locators, traces and the story gallery, and the test process stays in Node.js. Vitest runs tests natively inside the browser and needs a Playwright or WebdriverIO provider for CI. Choose by the runner your team maintains.

Can Playwright replace React Testing Library?

For real-browser component behavior, largely yes. Testing Library supplies DOM-focused queries while the runner, renderer and environment stay separate choices. Playwright's migration guide maps queries, waitFor, within, rerender and unmount onto locators and retrying assertions, but still shows the deprecated imports.

Ship continuously. Test continuously.

Qodex explores your app, writes runnable tests, and replays them on every change at zero LLM cost.