Skip to content
Esc
navigateopen⌘Jpreview
On this page

Browser Mode

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:

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:

// 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 covers that layout.

Test a real scroll transition

Use Vitest’s React Browser API 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.

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. That guide covers happy-dom, jsdom, Jest, Vitest, and both automatic and manual setup.

Was this page helpful?