# Add Dark Mode to a Gatsby Blog

Yes. An agent adds a dark theme to your Gatsby blog's CSS variables, a toggle that persists, and a no-flash script, then builds and screenshots it.

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

## What the agent does

The agent clones the site into its own cloud sandbox and reads where the colours live before it adds a theme. In a blog built from gatsbyjs/gatsby-starter-blog, `src/style.css` opens with a `:root` block that defines the whole palette as custom properties: `--color-primary: #005b99`, `--color-text: #2e353f`, `--color-text-light: #4f5969`, `--color-heading: #1a202c`, `--color-heading-black: black` and `--color-accent: #d1dce5`, next to the spacing, font and type-scale tokens. Every rule below reads those variables; `body` uses `--color-text`, links use `--color-primary`, `blockquote` uses `--color-text-light` with a primary-coloured border, and table headers draw their line in `--color-accent`. There is no `prefers-color-scheme` query and no `data-theme` attribute anywhere in `src/`. `gatsby-browser.js` imports the two fontsource packages, `normalize.css`, `style.css` and `prismjs/themes/prism.css`, a light code theme; `gatsby-ssr.js` has one hook, `onRenderBody`, which sets `lang` on the `html` element.

That layout decides the approach. The agent adds a second block in `style.css`, `:root[data-theme="dark"]`, that reassigns the six colour tokens plus a background variable it introduces for `body`, and sets `color-scheme: dark` so form controls and scrollbars follow. Code blocks need their own treatment because `prism.css` hard-codes light token colours: the agent adds the dark Prism token overrides under the same selector rather than importing a second theme, so there is one source of truth. In `gatsby-ssr.js` it extends `onRenderBody` with `setHeadComponents` to inject a small inline script that reads `localStorage.theme`, falls back to `matchMedia('(prefers-color-scheme: dark)')`, and sets `document.documentElement.dataset.theme` before the first paint, which is what stops the light flash on a static site.

The toggle goes in `src/components/layout.js`, the one component every page renders. Its `Layout` wraps `global-wrapper`, a `global-header` holding the `main-heading` link on the index or the `header-link-home` link elsewhere, `main` and a footer. The agent adds a `button` beside the header link with `aria-pressed` and an accessible label, a `useEffect` that reads the attribute set by the head script so the server-rendered markup and the hydrated state agree, and a click handler that flips `data-theme` and writes `localStorage.theme`. `gatsby-config.js` keeps `theme_color` commented out in `gatsby-plugin-manifest`, and the agent leaves it so, since a manifest colour cannot follow the toggle.

There are no tests to run: `package.json` has no test runner and its `test` script prints a reminder to write some and exits 1, and the repository has no `.github/` directory. So the check is the build. The agent runs `npm run build`, serves `public/` with `npm run serve`, opens the index, a post such as `content/blog/hello-world` and the 404 page in the sandbox browser in both themes, screenshots each, and reads the contrast of body text, links and `blockquote` text against the new background from the computed styles. It runs `npm run format`, which applies the `.prettierrc` settings `arrowParens: avoid` and `semi: false`, before it opens the pull request.

## Start it with one API call

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

```json
{
  "agent": "claude",
  "input": "Add dark mode to this Gatsby blog. Read src/style.css, src/components/layout.js, gatsby-browser.js, gatsby-ssr.js, gatsby-config.js and src/pages/index.js. Add a :root[data-theme=\"dark\"] block in style.css that reassigns the colour custom properties and a body background, sets color-scheme, and overrides the Prism token colours so code blocks are readable; keep the light values as they are. In gatsby-ssr.js inject a head script that applies localStorage.theme or the prefers-color-scheme match to document.documentElement before paint. Add a toggle button to the header in layout.js with aria-pressed that flips the attribute and stores the choice. Run npm run build and npm run serve, open the index, one post and the 404 page in both themes in the browser, take screenshots and attach them, check text, link and blockquote contrast against the dark background, run npm run format, and open a pull request with the screenshots in its description. Do not change fonts or spacing.",
  "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 with three changed files: `src/style.css` gains the dark block and Prism overrides, `src/components/layout.js` gains the toggle, and `gatsby-ssr.js` gains the pre-paint script. The description shows the index and a post in light and dark side by side, from the screenshots the agent took, and lists the colour values it chose for each token.

The screenshots are on the session as artifacts, each with a `url` that downloads it for an hour, and the transcript at `GET /o/{org}/sessions/{id}/messages` keeps the `gatsby build` output and every page the browser opened. `GET /o/{org}/p/{project}/sessions/{id}/changes` shows the diff per file.

Before merging, a reviewer loads the preview, reloads a post with the dark theme stored to confirm there is no flash, checks the toggle with a keyboard, and reads the Prism overrides against a post with a code block. A follow-up on the same session changes a colour or moves the toggle without starting over.

## Run it on every design review in the team

This is work a team steers rather than fires and forgets. One person starts the run from the console or with `POST /o/{org}/sessions`, and everyone who can see the Project follows the live stream at `GET /o/{org}/sessions/{id}/events` and the screenshots as they land. When the first pass is up, a designer sends a follow-up with `POST /o/{org}/sessions/{id}/messages` while the turn is finished, such as a different accent colour or a toggle in the footer instead of the header, and the sandbox wakes with the same checkout and `node_modules`.

For a site with several themes to try, a script sends one request per palette with a `request_id` made from the palette name, so each variant gets its own sandbox and pull request and a retried call returns the run already started. The key's webhook reports `run.completed` with `data.session_url` for each.

## Why run it on Rush?

Dark mode on a static site is judged by looking at it: whether the first paint flashes, whether code blocks stay readable, whether the toggle survives a reload. The agent builds the site with the repository's own `gatsby build`, opens the real pages in a browser in its sandbox, and attaches the screenshots, so the pull request is reviewed from pictures of the result rather than from a description of it.

The run is shared from the start. The stream, the screenshots and the diff are readable by the whole Project while it works, and the next request from a teammate goes to the same parked sandbox, which keeps a design discussion on one session instead of a chain of laptops.

## Frequently asked questions

### Will the page flash light before going dark?

The point of the script in `gatsby-ssr.js` is that it does not. It is injected into the head of every server-rendered page and sets `data-theme` on the root element from `localStorage` or the media query before the first paint, so the dark values from `style.css` apply on the first frame. The agent checks this by reloading a post in the sandbox browser with the dark choice stored.

### What happens to code blocks?

`gatsby-browser.js` imports `prismjs/themes/prism.css`, whose token colours are set for a light background. The agent overrides those tokens under `:root[data-theme="dark"]` in `style.css` instead of importing a second theme, so the dark palette for code lives beside the dark palette for text.

### Does it follow the operating system setting?

Yes, until the visitor chooses. With nothing in `localStorage` the head script uses `prefers-color-scheme`; the toggle writes the choice, and from then on the stored value wins. A reviewer can clear the stored value in the browser to see the system default again.

### Can it run the tests?

There are none to run: the starter's `test` script exits with an error and prints a reminder to write some. The build and the browser check are the verification, and the pull request says so. Writing Playwright checks for the toggle is a separate job on a separate page.
