---
title: Browser Mode
description: Use Vitest Browser Mode to test real scrolling, layout, focus, and native Intersection Observer behavior.
---

Vitest Browser Mode runs your component test in a real browser instead of a simulated DOM. The test gets real viewport geometry, CSS layout, scrolling, focus, and the browser's own `IntersectionObserver`.

Use it when the browser is part of what you need to prove. An element entering a scroll container, a `rootMargin` starting work early, a layout change moving a target, the native observer delivering an update: none of those are provable against a fake DOM.

## Keep Browser Mode separate from mocked DOM tests

Run Browser Mode tests in a Chromium browser project, separate from the `happy-dom` or `jsdom` tests that load `react-intersection-observer/test-utils`. The native observer has to stay in place, or the test proves nothing about scrolling, layout, or observer delivery.

## Install and configure Browser Mode

Install Vitest's Playwright provider, the React renderer, and a browser:

```bash
pnpm add -D vitest vitest-browser-react @vitest/browser-playwright playwright
pnpm exec playwright install chromium
```

Configure the test files and provider. Keep the `include` pattern narrow so these tests stay separate from the rest of the suite:

```ts
// vitest.config.ts
import { playwright } from "@vitest/browser-playwright";
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    include: ["src/**/*.browser.test.{ts,tsx}"],
    browser: {
      enabled: true,
      provider: playwright(),
      instances: [{ browser: "chromium" }],
    },
  },
});
```

Run the browser tests with your normal Vitest command. For CI, add `headless: true` inside `browser`. Without it, Vitest opens its browser UI, which is what you want locally.

If the repository also has deterministic DOM tests, add Browser Mode as one named project. The [testing overview](/testing) covers that layout.

## Test a real scroll transition

Use [Vitest's React Browser API](https://vitest.dev/api/browser/react) to render components and make retriable assertions in the browser. This example proves both sides of the interaction. The target starts outside the viewport, and only becomes visible after real scrolling changes the layout.

```tsx
import { render } from "vitest-browser-react";
import { expect, test } from "vitest";
import { useInView } from "react-intersection-observer";

function RevealOnScroll() {
  const [ref, inView] = useInView();

  return (
    <div ref={ref} data-inview={inView} style={{ height: 200 }}>
      Observed target
    </div>
  );
}

test("updates when the target scrolls into the viewport", async () => {
  const screen = await render(
    <>
      <div style={{ height: window.innerHeight }} />
      <RevealOnScroll />
      <div style={{ height: window.innerHeight }} />
    </>,
  );

  const target = screen.getByText("Observed target");

  await expect.element(target).toHaveAttribute("data-inview", "false");

  window.scrollTo(0, window.innerHeight);

  // Native IntersectionObserver delivery follows the scroll asynchronously.
  await expect.element(target).toHaveAttribute("data-inview", "true");
});
```

`expect.element` retries in the browser, so the final assertion waits for the observer callback instead of guessing at a timer value.

## Keep the native observer native

Do not import `react-intersection-observer/test-utils` in Browser Mode setup. Its mock replaces the browser API, and the test stops proving anything about scrolling, layout, or native observer delivery.

For a callback or threshold branch that does not need real geometry, use [mock intersections](/testing/mocking-intersections). That guide covers `happy-dom`, `jsdom`, Jest, Vitest, and both automatic and manual setup.
