Interactive Visual Review Site

Serve a self-contained React review site from css-visual-diff output for interactive visual comparison with local storage feedback.

Sections

Terminology & Glossary
📖 Documentation
Navigation
8 sectionsv0.1
📄 Interactive Visual Review Site — glaze help review-site
review-site

Interactive Visual Review Site

Serve a self-contained React review site from css-visual-diff output for interactive visual comparison with local storage feedback.

Appreviewservevisual-regressionreactembedservedata-dirsummaryporthost+1

The visual review site is an interactive single-page application that turns the artifacts produced by css-visual-diff — screenshots, CSS diffs, and comparison metadata — into a human-friendly review interface. Instead of browsing a folder of PNGs and JSON files, the reviewer opens a single URL and sees every comparison as an interactive card they can annotate, score, and export for further work.

The site is built as a React application compiled by Vite, embedded into the Go binary at compile time, and served through the css-visual-diff serve command. No Node.js runtime is needed at serve time. The binary is fully self-contained.

Why This Exists

Visual regression tools are good at measurement: they compute pixel differences, extract CSS property deltas, and classify sections by severity. But measurement is only half the job. The other half is human judgment: someone needs to look at the screenshots, decide whether each difference is acceptable, write notes about what to fix or accept, and pass those notes to a developer or coding agent.

The review site bridges measurement and judgment. It keeps the computed evidence — pixel percentages, CSS diffs, selector metadata — close to the human decision. Each comparison section gets its own card with images, metadata, a status dropdown, and a notes field. Everything the reviewer types is persisted in browser localStorage so it survives page reloads. When the review is done, the reviewer opens the export modal and copies a markdown + YAML block that contains all feedback in a format ready for an issue, a pull request comment, or an LLM prompt.

Quick Start

Build the frontend and embed it into the binary, then serve a comparison run:

# Build the React SPA through the Dagger-first pipeline and compile the CLI
make build-embed

# Serve a completed comparison run
css-visual-diff serve \
  --data-dir /tmp/my-comparison-run \
  --port 8097

Open http://127.0.0.1:8097 in a browser. The site loads the summary manifest from the data directory and renders one review card per page/section comparison.

The data directory should be the output of a previous css-visual-diff run or a verb-based comparison suite. It must contain subdirectories like <page>/artifacts/<section>/ with compare.json, left_region.png, right_region.png, and diff_only.png files. A summary.json at the root of the data directory provides the manifest that lists every row the reviewer should see.

The Serve Command

The serve subcommand starts an HTTP server that provides three things: API endpoints for the manifest and per-section comparison data, artifact file serving for PNG screenshots and JSON files, and the embedded React SPA itself.

Flags

FlagDefaultPurpose
--data-dir(required)Path to the css-visual-diff run output directory.
--summary<data-dir>/summary.jsonExplicit path to the summary JSON manifest.
--port8097HTTP server port.
--host127.0.0.1Bind address.
--openfalseOpen the browser automatically after starting.

API Endpoints

The React SPA communicates with the Go server through three endpoints. You do not normally call these directly, but they are stable and documented here for scripting or integration.

GET /api/manifest returns the summary JSON. This is the entry point the SPA fetches on load. It contains an array of rows, each with page, section, classification, changedPercent, and paths to every artifact file. The SPA rewrites these absolute paths into relative /artifacts/ URLs.

GET /api/compare?page=NAME&section=NAME returns the compare.json for a single page/section. The SPA loads this lazily when a reviewer expands a card. It contains the full comparison data: bounds, pixel counts, computed style differences, attribute changes, and source URLs for both prototype and React sides.

GET /artifacts/{page}/{section}/{file} serves a single artifact file from the data directory. The Go handler maps the three-part URL path back to the on-disk location <data-dir>/<page>/artifacts/<section>/<file>. This is how the SPA loads PNG screenshots and raw compare JSON.

SPA Fallback

Any request that does not match /api/ or /artifacts/ falls through to the embedded React SPA. Unknown paths serve index.html so that client-side routing works. The SPA assets are always compiled into the Go binary from internal/cssvisualdiff/review/embed/public/.

The Review Interface

When the page loads, the reviewer sees a header bar and a list of comparison cards. The header shows the total page and section counts, the worst classification, and classification tallies. Below the header, a toolbar provides view mode buttons and an "Add comment" toggle.

Review Cards

Each card represents one page/section comparison. The collapsed card shows the page and section name, the computed classification (a colored pill), and the changed percentage. A dropdown lets the reviewer set a human status: unreviewed, accepted, needs-work, fixed, or wont-fix.

Classification is computed by css-visual-diff from the pixel-change percentage. Status is the reviewer's own verdict. They are independent. A card can be classified as "tune-required" but accepted by the reviewer if the visual difference is intentional or acceptable.

Clicking a card expands it to show the image comparison area, a general observation textarea, and artifact links.

View Modes

The image comparison area supports four view modes, selected from the toolbar or by pressing keys 1 through 4.

Side-by-side (key 1) shows the prototype screenshot on the left and the React screenshot on the right. Each image has a label strip showing "prototype" or "react" and the source URL.

Overlay (key 2) stacks both images. The reviewer drags an opacity slider between A (prototype) and B (React). A "diff blend" toggle switches to CSS difference blend mode. Hold the F key to flash between the two images instantly. This is the fastest way to spot subtle alignment or color differences.

Slider (key 3) uses CSS clip-path to show the prototype on the left of a draggable divider and React on the right. The reviewer drags the handle to sweep across the image.

Diff only (key 4) shows only the diff_only.png artifact, which highlights the changed pixels. This is the best starting point for identifying where differences exist before examining the full screenshots.

Zoom and Pan

All view modes support zoom and pan for inspecting pixel-level details. Scroll the mouse wheel to zoom in and out (0.25x to 8x). The zoom tracks toward the cursor position. Hold Shift and drag with the left mouse button to pan around the zoomed image. Double-click to reset zoom and pan to the default view. A small indicator in the bottom-left corner shows the current zoom percentage and pixel offset.

Comment Pins

The reviewer can drop numbered annotation pins on any image. Click "Add comment" in the toolbar (or press P), then click on the image. Each pin has a type — issue, note, question, or praise — and a text field. Pins are visible as colored numbered circles on the image and listed in the sidebar Comments tab. The reviewer can change the type, edit the text, or delete a pin from the sidebar.

Pins are persisted in localStorage alongside the review status and notes.

The right sidebar has three tabs. Comments lists all pins for the current card with inline editing. CSS diff shows every computed CSS property that differs between prototype and React, with the left and right values side by side. Meta shows bounds, pixel counts, selectors, and source URLs.

The CSS diff tab is especially useful for answering "why does this look different?" — it shows properties like font-size, padding, background-color, and line-height with exact values from both sides.

Status and Notes

Each card has a status dropdown and a general observation textarea. Both are automatically persisted to localStorage. The status options are:

  • Unreviewed — no human decision yet (default).
  • Accepted — the difference is fine.
  • Needs work — something should be fixed.
  • Fixed — a change has been made after feedback.
  • Wont-fix — acknowledged difference that will remain as-is.

The note field is free-form text for observations that apply to the whole section.

Keyboard Shortcuts

The review site supports keyboard shortcuts for fast navigation without touching the mouse. All shortcuts are disabled when the cursor is inside a textarea, input, or dropdown.

KeyAction
jMove to the next card.
kMove to the previous card.
aMark current card as accepted.
nMark current card as needs work.
wMark current card as won't fix.
xMark current card as fixed.
1Switch to side-by-side view.
2Switch to overlay view.
3Switch to slider view.
4Switch to diff-only view.
eOpen the export modal.
pEnter comment pin mode.

The Export Modal

The "Send to LLM" button in the header (or pressing E) opens the export modal. This modal generates a markdown + YAML document containing all review feedback. The reviewer can choose to export all cards or only reviewed ones. The preview shows the full generated text.

The exported markdown includes, for each card:

  • The page/section name, classification, and changed percentage.
  • The reviewer's status decision.
  • Any general observation notes.
  • Computed style differences (property, left value, right value).
  • A YAML code block with structured metadata: bounds, pixel counts, classification, and all review comments.

Clicking "Copy markdown" copies the full text to the clipboard. The reviewer can then paste it into a GitHub issue, a pull request comment, a chat message, or an LLM prompt.

Local Storage Persistence

All reviewer feedback — status decisions, notes, and comment pins — is stored in the browser's localStorage under a key derived from the manifest content. This means:

  • Feedback survives page reloads and browser restarts.
  • Each comparison run gets its own storage namespace.
  • Clearing browser data removes all stored feedback.
  • Feedback is local to the browser and device. It is not synced to a server or shared with other users.

The storage key looks like cssvd-review-run-<hash>, where <hash> is derived from the page/section names in the manifest. This ensures that a different comparison run does not overwrite feedback from a previous run.

Build Pipeline

The review site uses a two-stage build: first the React SPA is built by Vite, then the output is copied into a Go embed directory and compiled into the binary.

The cmd/build-web tool uses Dagger to build the frontend inside a node:22 container. It creates a pnpm CacheVolume so that repeated builds reuse downloaded packages. This means Docker is required, but Node.js does not need to be installed on the host.

go run ./cmd/build-web

Local Build (Fallback)

If Docker is unavailable, set BUILD_WEB_LOCAL=1 to build using the locally installed pnpm:

BUILD_WEB_LOCAL=1 go run ./cmd/build-web

Generating and Compiling

The Go embed directory is populated by the build tool. To regenerate it and compile:

# Build frontend via Dagger, copy to embed directory, then compile dist/css-visual-diff
make build-embed

Use make build-web-local only when you explicitly want local Node/pnpm instead of the Dagger container.

The SPA is always embedded from internal/cssvisualdiff/review/embed/public/; there is no non-embedded filesystem fallback. If you change frontend code, rebuild the web assets and then rebuild the Go binary.

Development Workflow

During frontend development, run the Vite dev server and the Go server separately:

# Terminal 1: Go server
go run ./cmd/css-visual-diff serve --data-dir /tmp/my-run --port 8097

# Terminal 2: Vite dev server with HMR
cd web/review-site && pnpm dev

The Vite dev server runs on port 5173 and proxies /api and /artifacts requests to the Go server on port 8097. Edit React components and see changes instantly with hot module replacement.

Makefile Targets

The project Makefile provides these targets for common operations:

TargetWhat it does
build-webBuild the React SPA with the Dagger-first pipeline and copy to embed directory.
build-web-localBuild the React SPA with local Node/pnpm and copy to embed directory.
build-embedBuild the frontend and then compile the Go binary with it embedded.
dev-webStart the Vite dev server with hot reload.
dev-serveStart the Go serve command with test data on port 8098.

Data Directory Layout

The serve command expects the data directory to follow the structure produced by css-visual-diff:

/tmp/my-comparison-run/
  summary.json                         ← manifest listing all rows
  about/
    artifacts/
      content/
        compare.json
        left_region.png                ← prototype screenshot
        right_region.png               ← react screenshot
        diff_only.png                  ← changed-pixel highlight
        diff_comparison.png            ← side-by-side triptych
  shows/
    artifacts/
      content/
        ...
      header/
        ...

If the summary JSON is at a different location, use the --summary flag:

css-visual-diff serve \
  --data-dir /tmp/my-comparison-run \
  --summary /tmp/my-comparison-run.json

Troubleshooting

ProblemCauseSolution
"No embedded SPA found" messageThe embedded asset directory does not contain index.html when the binary was built.Run make build-embed, or make build-web-local if Docker is unavailable, then rebuild the CLI.
Cards load but images show 404Artifact paths in the summary JSON do not match the data directory structure.Ensure --data-dir points to the directory containing <page>/artifacts/<section>/ subdirectories.
Images not loadingThe Go server is not running, or the port is wrong.Verify the server is running and the browser is accessing the correct port.
Export modal is emptyThe summary JSON has no rows, or all rows have empty data.Run css-visual-diff with --summary to produce a valid manifest.
Keyboard shortcuts do not workFocus is inside a textarea, input, or dropdown.Click outside the input field and try again.
Zoom is stuck at one levelThe scroll event is being captured by a parent scrollable element.Place the mouse directly over the image area and scroll.
localStorage notes disappearedBrowser data was cleared, or the manifest changed (different hash).Re-open the same comparison run. Previous feedback is lost only if the browser data was cleared.
Dagger build failsDocker is not running or not installed.Start Docker, or use BUILD_WEB_LOCAL=1 as a fallback.

See Also

  • css-visual-diff help site-comparison-workflow — How to generate review-site data from a YAML spec and serve it.
  • css-visual-diff help review-site-data-spec — The summary.json, compare.json, and artifact directory contract.
  • css-visual-diff help javascript-verbs — How verb scripts drive comparison suites that produce the data consumed by this review site.