Vitest Browser Mode - The Future of Frontend Testing

November 30, 2025
#testing#frontend#vitest#vitest-browser-mode

One of the most exciting developments in the last few years when it comes to testing is definitely Vitest Browser Mode . In this article I am going to tell you everything you need to know.

And this is such a big change in the JS testing ecosystem - I believe that within a year or two, all frontend engineers will need to know what Vitest Browser Mode is just like we all have to at least be aware of Jest/Vitest test runners, Cypress/Playwright E2E tests etc.

What are Jest, Vitest, Playwright or Cypress? (Show details)

Good questions!

  • Jest and Vitest are test runners. They are very similar. This is where you run your expect(val).toBe(expectedVal) sort of tests. When testing React components in these, they run in your terminal with a fake simulated DOM.
    • Note: Vitest comes from the team that released Vite which is a tool for building frontend JS apps.
  • Playwright/Cypress are end to end (E2E) frameworks, which runs in headless mode in real browsers (Chrome etc)

This blog post is about Vitest Browser Mode - it is a special way of running tests within Vitest.

If you are very new to all of this, check out my completely free 19 lessons on Vitest fundamentals

Vitest Browser Mode combines several powerful features:

  • Real browser testing... runs tests in real browsers (Chrome, Firefox) like E2E tests
  • Component isolation... test single components at a time, just like normal Vitest/Jest tests
  • Visual preview... easily see the rendered components that you are testing
  • Fast execution... despite running in a real browser, which at first might seem like it would be slow - it is actually really fast.
  • Built-in Visual Regression screenshots... take screenshots of your components and check that they are rendering the same in future test runs

This is different to running E2E (End to end) tests directly in Playwright.

With Vitest browser mode we are still testing individual components by themselves. Just with a real browser. So all the web APIs like session storage, cookies, local storage, fetch requests, URL manipulation, clipboard API, geolocation, web workers and more just work out of the box now!

If you are new to this site: hi! I am a software engineer who loves writing tests.

I created this site to help onboard people into writing tests (specifically frontend tests).

You're on the blog, where I try to only post high quality deep dives (like this one!).

Very quick overview

  • Write your tests in a way that is similar to React Testing Library and Playwright (it is like a mix of both). A lot more async behaviour!
  • Can test a single component at a time
  • It runs in a real browser. No need to mock web APIs! Very realistic testing.
  • As it is in a real browser, you can preview what your tests are rendering in a UI
  • And of course you can run it on your CI/CD pipelines

Here is a demo of the UI for it (but remember - it can run in your terminal as well in headless mode).

Demo showing how to use Vitest Browser Mode in a real browser to preview what your tests are doing

My prediction - Vitest Browser Mode will be a standard part of FE testing soon!

It is the end of November 2025 right now. Vitest came out with version 4 last month, which marked Vitest Browser Mode as stable. So it is quite new.

I predict that by November 2027 Vitest Browser mode will become a standard part of testing frontend React applications. I do not think it will completely replace the typical way (Vitest/Jest with React Testing Library) by then. I'll come back in a couple of years and see how this turned out...

Anyway, onto the rest of this Vitest Browser Mode introduction/tutorial/setup guide!

How Vitest Browser Mode works

When you test React components with 'regular' Vitest (or Jest), you will normally mount/render your components in a simulated DOM (often jsdom ).

This is (very!) good for most components. But it isn't completely realistic.

If you have ever wanted to test components which use Web APIs like:

  • window.sessionStorage or window.localStorage
  • things like MutationObserver, IntersectionObserver or ResizeObserver
  • copy/pasting with navigator.clipboard
  • window.location and URL manipulation

...then you've probably come across some limitations of these 'fake' DOMs that run in your terminal.

You normally end up either manually mocking them, or adding a npm package (which will just mock it for you).

Also, if a test breaks it can be quite awkward with seeing the rendered HTML and figuring out why it isn't working as expected.

Of course, we've always had End-to-end (E2E) test frameworks (Playwright and Cypress are two of the most popular tools). They will run a headless browser (Chrome, Firefox etc) on a real page, and of course this means it has access to test all the real web APIs.

Vitest basically runs (headless or in UI mode) a real browser, and runs your component inside an iframe. So you can control the viewport size, and you get all the real CSS and Web API support.

Your test file can interact with the DOM (although it is recommended to use helper functions and not directly interact with the DOM).

You can use all the normal Vitest syntax, plus the special syntax and rendering functions to ensure you can fully control what happens in the browser rendering your component.

How to write tests for Vitest Browser Mode

It is very much inspired by React Testing Library - the functions like getByRole(), getByTestId() and so on will look familiar to you if you have used React Testing Library.

In fact, they are based on Playwright's locators, which are implemented via a library called ivya .

One big difference to the RTL query functions is how everything is async and promise-based so the syntax is a little different.

So you will end up doing a lot more await, and you need to get the element (so you do page.getByRole(...).element(), or just expect.element(page.getByRole(...)).

Here is an example component showing some of the syntax:

import { expect, it } from 'vitest';
import { render } from 'vitest-browser-react';
import { page } from 'vitest/browser';
import { useState } from 'react';

const CounterComponent = () => {
  const [count, setCount] = useState(0);
  return (
    <div>
      <h1>Count: {count}</h1>
      <button
        onClick={() =>
          setCount(count + 1)
        }
      >
        Increment
      </button>
    </div>
  );
};

it('should let you increment', async () => {
  render(<CounterComponent />);

  // everything is promise based:
  await expect
    .element(page.getByRole('heading'))
    .toHaveTextContent('Count: 0');

  // get an element, then call .click() on it directly
  // (no need for userEvent.click(button)
  const button =
    await page.getByRole('button');
  await button.click();

  await expect
    .element(page.getByRole('heading'))
    .toHaveTextContent('Count: 1');
});

This blog post is a high level intro to Vitest Browser Mode. Join my free FE testing newsletter to find out when I post a full course on how to use Vitest Browser Mode!

There is already a prerelease version you can see (if you are a member here) covering the main lessons in detail, but I still need to polish it a bit (I expect it to be fully ready around the start of December 2025)

Vitest Browser Mode Providers

A provider in this context means where/how you actually run your tests in the browser.

There are three providers to choose from right now - but you should almost definitely use the playwright one

Here are the options:

  • playwright - this is my recommended provider. You use Playwright to control browsers (headless or not) like Chrome. It works well locally as well on CI/CD.
  • preview - good for initial setup and for local only tests. It is a bit easier to configure (it just works out of the box), but it won't work on CI/CD (like Github Actions), has no headless support, doesn't support multiple browser instances and more. It is not recommended to use, really
  • webdriverio - useful mainly if you use WebdriverIO already. If you don't already use WebdriverIO, I would not recommend using it for Vitest Browser Mode

So despite the choice of 3 - your best bet is to stick with Playwright.

Packages to install

BTW I have a full working github repo with a React app tested in Vitest Browser Mode, and it all working on CI/CD to demo it. If you are setting this up in your app you might want to check it out - see HowToTestFrontend/vitest-browser-mode-starter-react

I am assuming you already have Vitest installed. (If not: there are tons of guides out there. Basically just install vitest and set up your vitest.config.ts file.

Once you have Vitest installed, you should install these packages to get Vitest Browser mode working. I am going to continue this demo to get it set up for a React / NextJS based app.

npm install --save-dev @vitest/browser @vitest/browser-playwright @vitest/ui vitest vitest-browser-react

The specific versions of all related packages that I am using for this demo are:

  • @vitejs/plugin-react: 5.1.1
  • @vitest/browser: 4.0.13
  • @vitest/browser-playwright: 4.0.13
  • Installing a Playwright package because we are going to use Playwright as the provider to run the headless browser control
  • @vitest/ui: 4.0.13
  • Optional, but useful to be able to run it in UI mode locally so you can preview what is happening in your tests
  • vitest-browser-react: 2.0.2
  • (this is used to give us a render function so we can call await render(<YourComponent />)).

And these are some related ones that you may already have installed:

  • react: 19.2.0
  • react-dom: 19.2.0
  • vite-tsconfig-paths: 5.1.4 (so our tsconfig path aliases continue to work)
  • vitest: 4.0.13

Vitest browser mode config

If you already have a Vitest config (Show details)
  • You can (and probably should!) have a different Vitest configuration file just for browser mode.

  • The way I prefer to set it up is to have your normal vitest.config.ts, and then make another one called vitest.browser.config.ts (or .mts file endings)

If you don't have an existing Vitest config (Show details)
  • You can follow along, just create a vitest.browser.config.ts (or vitest.browser.config.mts)

  • You can definitely run Vitest Browser Mode in an app that normally uses Jest. You will use Vitest just for the Browser Mode tests.

To simplify things and keep the Vitest Browser Mode tests separate to the other test files, I've gone with a *.browser.ts and *.browser.tsx filename for your tests.

This is so that you can easily filter test files just for the browser mode config, leaving you existing tests (which are probably named *.test.tsx or *.spec.tsx) alone.

You can change these settings to whatever suits your need.

Sometimes *.spec.ts are used for e2e tests, so that convention could also work for Browser Mode tests.

As far as I can tell there is no standard way yet that most projects follow - if you know of one, please reach out to me and I'll update this blog post.

This is my vitest.browser.config.ts:

import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';
import { playwright } from '@vitest/browser-playwright';

export default defineConfig({
  plugins: [tsconfigPaths(), react()],
  test: {
    // run this before each test. In this case it is used to ensure that the css is loaded
    // Note: you will need to create this file - see the repo for an example
    setupFiles: [
      'vitest.browser.setup.ts', // or whatever filename you gave your setup file
    ],

    // list of file name extensions used for browser tests:
    include: [
      './**/*.browser.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
    ],

    // so we don't have to import describe, expect, etc:
    globals: true,

    // config for browser mode:
    browser: {
      enabled: true,
      headless:
        process.env.CI === 'true',
      provider: playwright(),
      screenshotDirectory:
        'vitest-test-results',
      instances: [
        { browser: 'chromium' }, // playwright supports chromium, firefox, or webkit
      ],
    },
  },
});

I also created a vitest.browser.setup.ts file, which runs before any tests. This is used in my demo starter repo to import the CSS - see the file here .

Running the tests

Before running the tests you have to remember to install Playwright browsers (Chromium etc). You only have to do this once.

If you are using yarn, the command is:

yarn playwright install --with-deps

Then to run the browser mode tests on your machine, run this:

yarn vitest --browser --config=vitest.browser.config.ts --ui
# (or vitest.browser.config.mts if you name it that)

If you have followed along with the config files above, this will look for all files ending in *.browser.tsx.

If you want a demo test file that will pass tests, pull this Vitest Browser Mode test into your codebase.

More commands to run (Show details)

If you are looking for a few more options (put these in a scripts script in your package.json):

  • The basic command is just vitest --browser
  • To run it in a UI when you have headless:true in the config, use vitest --browser --ui
  • To run it with npx: npx vitest --browser
  • To run in a specific browser: npx vitest --browser=chromium or npx vitest --browser=chromium --ui
  • If you have a custom config file for your Vitest tests then run vitest --config=vitest.browser.config.ts

To see them in action, check out package.json in my Vitest Browser Mode starter repo.

Writing your tests

In my sample starter kit repo on Github for Vitest + React, I have two test files:

As React Testing Library (RTL) is so popular, I will assume many readers of this blog are familiar with it. So I will compare writing a test in Vitest Browser Mode and how you would have done the same with React Testing Library.

Importing the render() function

In Vitest + React Testing Library, you import like this:

import {
  render,
  screen,
} from '@testing-library/react';

In Vitest Browser Mode it is a bit different.

The main render function for React is imported like this:

import { render } from 'vitest-browser-react';

The call to render is very similar, although it now returns a promise.

In React Testing Library:

import {
  render,
  screen,
} from '@testing-library/react';

test('some component renders', () => {
  render(<Component />);

  expect(
    screen.getByText('something')
  ).toBeInTheDocument();
});

In Vitest, there is no import from RTL. And no direct screen object with all the getByText.

Instead, you can get an equivalent of screen from the return type of render(). Note: you will need to await the render() call!

import { render } from 'vitest-browser-react';

test('renders header', async () => {
  const screen = await render(
    <HomePage />
  );

  await expect
    .element(
      screen.getByRole('heading', {
        name: 'Welcome to this sample app',
      })
    )
    .toBeInTheDocument();
});

Alternative syntax - get a page object:

You can also import a page object (from vitest/browser), which works a bit like importing screen from the RTL module.

(If you have worked with E2E frameworks like Cypress or Playwright, this will look familiar to you.)

Import it from vitest/browser, then you can run your query functions on this page object like this:

import { render } from 'vitest-browser-react';
import { page } from 'vitest/browser';

test('example', async () => {
  render(<YourComponent />);
  const button =
    await page.getByRole('button');

  await button.click();
});

Async behaviour of (almost) everything

You will also notice the use of await (for the render() call, and expect.element()) in the examples.

When you call the query functions like screen.getByRole(), this is an async function.

It does not return the HTMLElement like React Testing Library's getByRole() does!

Instead it return a Locator object.

If you need to get the HTMLElement then you can do it by calling the element() function on the Locator object.

import { useState } from 'react';
import { expect, test } from 'vitest';
import { render } from 'vitest-browser-react';

const Component = () => {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h1>Count: {count}</h1>
      <button
        onClick={() =>
          setCount(count + 1)
        }
      >
        Increment
      </button>
    </div>
  );
};

test('can get current count', async () => {
  const screen = await render(
    <Component />
  );

  const count =
    screen.getByRole('heading');
  expect(
    count.element()
  ).toHaveTextContent('Count: 0');

  // but the more typical way in Vitest Browser Mode is expect.element()
  await expect
    .element(count)
    .toHaveTextContent('Count: 0');
});

But as you can see in the test above - there is a nicer shortcut: await expect.element().

This will keep retrying to get the element so the assertion (e.g. .toHaveTextContent('Count: 1')) passes, or until it times out in which case it will fail the assertion.

To avoid flakey tests, and to keep with Vitest Browser Mode conventions, most of your assertions should be with await expect.element(...)...

expect.element(locator) is a shorthand for expect.poll(() => locator.element()). You can kind of think of waitFor(...) (in React Testing Library) as the same as expect.poll(...).

Where are the findBy and queryBy functions?

I've got quite a few lessons on React Testing Library's getBy vs queryBy vs findBy functions. You are probably familiar with them.

quick recap on React Testing Library getBy/queryBy/findBy (Show details)

In React Testing Library, we have these type of query functions:

  • getBy (getByText() etc) is synchronous, and will throw an error if not exactly 1 result found
  • queryBy (queryByText() etc) is synchronous, it must either return no result or exactly 1 result. If no results, then it returns null
  • findBy (await findByText() etc) is asynchronous, and will retry for a short duration until it finds exactly 1 element or throws if it cannot find it

In React Testing Library there are also getAllBy/queryAllBy/findAllBy which return an array instead of just 1 item. The equivalent in Vitest Browser Mode is using the .all() method on the locator. For example, instead of getAllByRole('button') in RTL, you would use getByRole('button').all() which returns an array of all matching elements.

For the getBy (getByText() etc) it is quite similar (as long as you understand the .element() part above).

Due to how Vitest Browser Mode is mostly all async based, the syntax for equivalents of queryBy (for testing when it isn't on the page) and findBy (async) are different.

For query by to check something is NOT on the page, you have to use the .query() method on the Locator object.

(I promise that after writing a few tests like this, it is easy to pick up!)

test('can get current count', async () => {
  const screen = await render(
    <Component />
  );

  const countOf1 =
    screen.getByText('Count: 1');

  // confirm it isn't on the page at the start
  // use .query() on the Locator object:
  expect(countOf1.query()).toBeNull();

  await screen
    .getByRole('button')
    .click();

  // but now the locator can be used with `await expect.element(...)`
  // to check it is in the document
  await expect
    .element(countOf1)
    .toBeInTheDocument();
});

For the RTL findBy equivalents in Vitest Browser Mode you can just use getBy as it is all async.

This blog post is meant to be a guide to show the basics, and hopefully get you excited about this new way of testing. If you want to learn the ins and outs of Vitest Browser Mode, check out my full course with multiple lessons on each feature of Vitest Browser Mode.

Vitest Browser Mode FAQs

  • Do I need to replace my existing tests with Vitest Browser Mode tests?
    • No! You can add new tests that use Browser Mode when it makes sense to, alongside your regular/existing tests (see my github repo for an example of running them side by side)
  • Is it slower?
    • In theory yes. But I have been consistently impressed and surprised by just how quick and comparable it is to running tests in normal Vitest (with JSDom etc)
  • Is Vitest Browser Mode only for React?
    • All of my examples are with React. But it works well with Vue, Svelte and other languages.
  • Is Vitest Browser Mode ready for production use
    • Yes! It is stable, and has been used in production on many apps for a long time now. I have yet to find any reason to not use this in production.

How to start using Vitest Browser Mode in your project today

Until recently it wasn't really stable (even though for most users it would be fine). But since Vitest v4 it is marked as stable. You can start using it right now on your local machine and on your CI/CD pipeline (like GitHub Actions).

You can also start using it alongside any existing tests (Vitest, Jest, etc.). The only thing to consider is to make sure that your tests for Vitest Browser Mode are unique enough (so I've gone with *.browser.tsx for my Browser Mode test files).

You can follow the instructions above - they cover the basics.

I have a Github Action workflow here that I think is the easiest way to get set up. Just copy over the config files.

Most of the useful parts are in:

Confused by this or want to learn more?

This blog post is meant as an intro to help you grasp the very basics.

If you have any questions please reach out to me.

Found this useful? Share this article

TwitterLinkedIn Facebook

Thanks for reading!
Maybe you would like to subscribe to my FE testing newsletter?

If you found this blog post useful or you learned something, you might like my free newsletter, keeping you up to date with frontend testing.

I cover topics like testing frontend apps, how to write Vitest (including the new Vitest Browser Mode), high quality tests, e2e tests and more - with actionable tips and tricks to make sure your tests are as high quality as they can be!

I only send it every couple of weeks, never spam, and you can of course unsubscribe at any time.

Want to become a pro at testing your React apps?

I've got a huge range of interactive lessons from beginner to expert level.