# Giving Claude Code a browser: Playwright and Chromium in the container

Claude Code in my container can read, write and build the frontend code. What it couldn't do is look at the result. For backend work that's fine: tests pass or they don't, and `curl` shows exactly what the API returns. For layout, "the code looks right" and "the page looks right" are two very different claims, and only I could check the second one.

## Four PRs for one search box

The blog's header has a search box next to the Blog and Home links. It was taller than the links beside it. It took four PRs on the same day to fix:

1. A merge left a stray piece of a JSX comment behind and broke the build.
2. The input was `type="search"`, which carries its own browser styling that ignored the height it was given. Switched to `type="text"`.
3. Still wrong, and differently wrong in each browser I tried. The height was a percentage, resolved against a flex parent, which is a known cross-browser trouble spot. Switched to a fixed height.
4. DevTools showed the input itself was now exactly 40x40, but the row was still off by a few pixels. The `<form>` around the input kept its default browser margin. Made the form `display: contents`, so the input became a direct child of the header row.

Every one of those reasonings was sound, and Claude even checked the deployed HTML with `curl` each time to rule out a stale deploy. But `curl` shows the markup, not the layout. The real feedback loop was me: open the site, take a screenshot, report the symptom, wait for the next attempt.

## The Dockerfile change

Right after the Claude Code CLI, before switching to the non-root user:

```dockerfile
# --- Playwright: Chromium + all required Linux libraries ---
ARG PLAYWRIGHT_VERSION=1.57.0

ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright

RUN npm install -g playwright@${PLAYWRIGHT_VERSION} \
    && playwright install --with-deps chromium \
    && chmod -R a+rX /ms-playwright \
    && rm -rf /var/lib/apt/lists/*
```

A few choices in there:

- `--with-deps` also installs the Linux system libraries Chromium needs to run, not just the browser itself.
- `PLAYWRIGHT_BROWSERS_PATH` puts the browser in a shared location instead of under root's home, and `chmod -R a+rX` makes it readable for everyone. Together they let the non-root `claude` user launch it.
- The version is pinned, so a rebuild doesn't silently pull in a different Playwright and browser.

One small catch: a globally installed npm package isn't found by an ES module script just via `NODE_PATH`. In a quick script, load it like this:

```js
import { createRequire } from 'module';
const { chromium } = createRequire('/usr/lib/node_modules/')('playwright');
```

## What a check looks like now

The blog UI is a Cloudflare Worker, so Claude runs it locally with `wrangler dev` and points Playwright at it. For the search box, the useful question isn't "does it look OK" but "is it the same height and at the same position as its neighbours", and that's something a script can measure:

```js
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
await page.goto('http://127.0.0.1:8787/', { waitUntil: 'networkidle' });
const result = await page.evaluate(() => {
  const box = el => { const r = el.getBoundingClientRect(); return { top: r.top, h: r.height }; };
  const input = document.querySelector('header input');
  const row = input.closest('form').parentElement;
  return { input: box(input), links: [...row.querySelectorAll('a')].map(box) };
});
```

Running that against the current header, at desktop width (1280px) and phone width (390px), locally and against the live site, gives the same answer every time: the input, the Blog link and the Home link all start at 16px from the top and are 40px tall. That's the check the four PRs above were missing, and it runs in a second instead of in a round trip through me.

It can also take a screenshot, so when a number looks off Claude can look at the actual rendering instead of guessing from the markup.

## What it doesn't replace

It's Chromium only. Part of the search box saga was that each browser showed a different symptom, and a Safari rendering issue won't show up in headless Chromium. Playwright can install WebKit and Firefox too if that becomes worth the extra image size. For now, Chromium catches the "is it even the right size" class of problems, and I still do the final look myself.

Combined with the previous post, the loop is now: Claude makes the change, checks it in a real browser and pushes its branch, and I review and approve the pull request.

---

*Co-authored with Claude.*
