Blog
Home

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

25 September 2026 · 17 views

Part 6 of 6 in Running Claude Code in Docker

← Letting Claude Code push its own branch to GitHub

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:

# --- 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:

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:

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.

Related articles

Comments

← All articlesView as Markdown