# Add a Dark Mode Toggle to a Vite React App from a Linear Issue

Yes. An agent picks up the Linear issue, adds a dark mode toggle to your Vite React app's header with Vitest tests, and opens a pull request.

[Run this on Rush](https://cloud.getrush.ai/run/add-dark-mode/vite-react/), in a Project that has your repository.

## What the agent does

The agent starts from the issue and the repository, cloned into its own sandbox, and reads how the app styles itself before it adds a button. In bulletproof-react's `apps/react-vite` workspace (Vite 5, React with React Router, Tailwind `^3.4.3`, Vitest 2, MSW 2, yarn), `src/index.css` already defines the theme twice: color variables such as `--background` and `--foreground` under `:root`, and the same variables with dark values under a `.dark` selector. `tailwind.config.cjs` maps `background`, `foreground`, `border` and the rest to `hsl(var(--...))`. What it does not set is `darkMode`, so Tailwind 3 uses its default, the `media` strategy, which follows the operating system and ignores a `.dark` class. Half the work exists already; the agent says so and adds the other half.

The change itself is small and goes where the codebase expects it. It sets `darkMode: 'class'` in `tailwind.config.cjs` so the `.dark` variables and any `dark:` utility follow a class on `<html>`. It adds a small theme module that reads `localStorage`, falls back to `window.matchMedia('(prefers-color-scheme: dark)')` when nothing is stored, and writes the choice back. It puts the toggle in the shared layout, in the `<header>` of `src/components/layouts/dashboard-layout.tsx` next to the existing user menu, built from the project's own `Button` in `src/components/ui/button` with a `lucide-react` icon, as the rest of the header is. File and folder names stay kebab case, because `.eslintrc.cjs` enforces `KEBAB_CASE` with `check-file`, and the module stays out of `src/features/` because `import/no-restricted-paths` limits what features may import from each other.

Two details prevent the bugs a dark mode toggle usually ships with. A choice read in a React effect applies after the first paint, so a user who picked dark sees a white flash on every load; the agent adds a few lines of inline script in `index.html`, before `/src/main.tsx` loads, that set the class from `localStorage` or the media query. And jsdom has no `matchMedia`, which `src/testing/setup-tests.ts` does not stub today (it stubs `ResizeObserver`, `btoa` and `atob`), so the agent stubs it in its tests to cover both system preferences. The setup file also calls `vi.mock('zustand')`, so if the theme lives in a zustand store, its state resets between tests.

Then the tests, in a `__tests__` folder beside the component as `src/components/ui/dialog/__tests__/dialog.test.tsx` does: on first load with nothing stored, the class follows `prefers-color-scheme`; clicking the toggle switches the class on `document.documentElement` and stores the choice; a stored choice wins over the system setting. Last, it runs what `.github/workflows/react-vite-ci.yml` runs in `apps/react-vite`: `mv .env.example .env`, `yarn install`, `yarn build` (which is `tsc && vite build --base=/`), `yarn test`, `yarn lint` and `yarn check-types`, and it opens the dashboard in the sandbox's browser to take a screenshot in each theme.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "A Linear issue labeled agent says: \"Add a Dark Mode Toggle to a Vite React App. Add a toggle in the app header that switches the Tailwind theme, remembers the choice in localStorage and respects prefers-color-scheme on first load. Work in apps/react-vite.\" Before writing code, read how the app defines its theme today: the CSS variables, any .dark selector, and the darkMode setting in the Tailwind config, and say what already exists. Use the class strategy, put the toggle in the shared layout's header using the project's own Button and icon library, and follow its lint rules for file names and import boundaries. Apply the stored or system theme before React renders so there is no flash of the wrong theme. Add Vitest and Testing Library tests for the first-load default under both system preferences, the toggle switching the class and storing it, and a stored choice winning, stubbing matchMedia. Run yarn build, yarn test, yarn lint and yarn check-types in apps/react-vite until they pass, take a screenshot of the dashboard in each theme, and open a pull request that links back to the issue.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
```

1. **Get an API key.** In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
2. **Send the request.** POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
3. **Read what came back.** Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

## What comes back

What comes back is a pull request on its own branch that links the Linear issue. In an app shaped like bulletproof-react, the diff touches `tailwind.config.cjs` (one line, `darkMode: 'class'`), `index.html` (the script that sets the class before the bundle loads), a new theme module and toggle component under `src/components/`, the header in `dashboard-layout.tsx`, and a new test file in a `__tests__` folder. The description says what already existed (the `.dark` variables in `src/index.css`), what was missing (the class strategy and the toggle), and shows the dashboard screenshots in light and dark.

The proof is the checks and the transcript. The session keeps every command with its output: `yarn install`, each `yarn build`, `yarn test`, `yarn lint` and `yarn check-types`, including runs that failed before a fix. The repository's `react-vite-ci.yml` workflow runs the same build, test, lint and type check on the pull request in GitHub Actions.

Before merging, a reviewer loads the preview with the system set to dark and nothing stored, then toggles and reloads, to see that there is no flash; checks that components which hard-code colors instead of using the theme variables still read well in dark; and confirms the new tests fail when the toggle stops writing to `localStorage`.

## Run it on every Linear issue labelled agent

In the console, an automation on the Project with a Linear trigger on `linear.issue.labeled` and the label `agent` starts one run per labelled issue in the Project's repository. Product requests like this one, small and well described, are what the label is for: the issue text is the prompt.

From your own server, start the same run with a `POST /o/{org}/sessions` per issue, listing the repository and the issue's `https://linear.app/<workspace>/issue/<ID>` URL in `resources` so Rush reads the issue through your Linear connection. A webhook on the API key posts `run.completed` or `run.failed` with the `session_url` when the turn finishes.

## Why run it on Rush?

A dark mode change is visual, so it is finished when someone has looked at it, not when the tests pass. In a Rush sandbox the agent builds the app, runs it, and opens the dashboard in its own browser in both themes, so the pull request arrives with screenshots instead of a request to check it out locally. It runs the full yarn install, build, test, lint and type check on its own machine without touching anyone's dev server on port 3000.

The label in Linear is the only step a person takes. The transcript shows the agent finding the existing `.dark` variables before writing new ones and every check with its output, and the pull request links back to the issue. Several labelled issues can run at once, each on its own branch in its own sandbox, up to the key's `max_concurrent` limit.

## Frequently asked questions

### Will it use Tailwind's class or media strategy?

The class strategy, because a toggle has to override the system setting. With `darkMode: 'class'` the `.dark` class on `<html>` drives the theme, and the agent's script sets that class from the stored choice or from `prefers-color-scheme` when nothing is stored.

### How does it avoid the flash of the wrong theme on load?

It sets the class in a small inline script in `index.html` that runs before the React bundle loads, so the first paint already has the right theme. Reading the choice inside a React effect would apply it only after the page had rendered once in light.

### Does it work with shadcn/ui-style CSS variables?

Yes, that is the common case. When the theme is defined as CSS variables under `:root` and `.dark`, as bulletproof-react's `src/index.css` does, the agent changes no colors at all: switching the class switches every variable.

### How do the tests check prefers-color-scheme in jsdom?

jsdom has no `matchMedia`, so the tests stub `window.matchMedia` to answer dark or light and render the app's theme setup under each. They also set and clear `localStorage` to check that a stored choice wins over the system preference.
