Skip to content

Repository files navigation

tether

Visual QA for AI-coded interfaces.

tether learns the recurring visual rules in your frontend, then flags unexpected colors, spacing, typography, shape, and layout changes after code changes.

npx tether-ui init
npx tether-ui check
npx tether-ui report

Check UI changes against a baseline

tether workflow: baseline, UI changes, detected drift, and the real local report

Quick start

Node.js 20.9+ · Chromium · a running frontend

In your frontend's repository, install the package and Chromium:

npm install --save-dev tether-ui
npx playwright install chromium

Start your frontend, then save its current styles as the baseline:

npx tether-ui init --url http://localhost:3000

After changing your UI, check for drift and open the report:

npx tether-ui check
npx tether-ui report

The package is tether-ui; its CLI executable is tether. The report opens at localhost:4317. Use --no-open to suppress browser launch or --port 4320 for another report port.

--cwd /path/to/app selects a different project directory. init prompts for a URL in an interactive terminal when there is no config; otherwise it uses http://localhost:3000. An existing baseline is never overwritten unless you pass init --force.

See visual drift

The included forma demo has a clean state and five deliberate CSS changes. Run it locally to compare them.

The actual clean and drifted Forma interfaces, with the changes annotated

Why tether

A working page can still pick up an unexpected color, radius, or button height. tether points to the element and changed property so you can review it, whether the code came from a person or an agent.

Checks What tether looks for
Color Visible text, backgrounds, and border colors outside the palette
Typography New sizes, families, weights, and explicit line heights
Spacing Margin, padding, and flex/grid gaps outside the inferred scale
Shape Unexpected corner radii, shadows, and border combinations
Controls New rendered button, input, select, textarea, and switch heights
Layout Horizontal overflow, off-screen elements, and obvious vertical text clipping

The demo introduces exactly five CSS mutations: a rogue accent, a 20px card radius, 15px heading type, a 13px gap, and mobile overflow. A single mutation can have several consequences: changing type size can change line height; overflow can affect both a card and the document.

Review the report

A local report, backed entirely by your latest scan. Review deviations, filter by category and severity, copy an agent brief, or inspect viewport captures. The score is a rough prioritization aid: 100 − (8 × high + 4 × medium + 2 × low), clamped to zero. Findings repeated across viewports count once.

Overview report showing real visual drift

Saved styles

The visual contract renders the palette, type scale, spacing values, radii, shadows, borders, and control heights discovered in your baseline.

The actual inferred visual contract

How it works

  1. Observe. Playwright loads each route at every configured viewport in a fresh, light-mode browser context. After load and font readiness, it disables CSS motion, waits a short settling period, and reads computed styles.
  2. Normalize. Pixel values within 0.05px of an integer snap to it; other pixels round to one decimal. RGB/RGBA/hex normalize to uppercase hex (with alpha when needed). Fully transparent colors collapse to one value. Font weights and whitespace normalize too.
  3. Infer. A token normally needs two distinct elements. Repeated properties and repeated viewport captures do not inflate its count. If a category has no recurring token, its most frequent value is retained so a small interface still has a usable contract.
  4. Compare. Values outside the contract are findings, except unchanged values at the same route, viewport, selector, and property in the baseline. This preserves existing one-off choices without legitimizing them everywhere.
  5. Explain. Findings carry a stable ID, selector, CSS property, current value, expected tokens, route, viewport names, and deterministic severity.

Near-identical colors (at most two RGB channel levels apart) and subpixel dimensional differences (at most 0.25px) are tolerated. Spacing is only collected from box spacing and actual flex/grid gaps. Margins larger than 96px in magnitude are excluded as likely layout offsets (including browser-resolved auto margins). Inherited text styles are counted only on direct text elements and controls. Hidden elements, SVG internals, transparent values, zero dimensions, and explicit ignores are omitted. Layout problems remain visible even when they already exist in the baseline.

Severity is intentionally simple: major overflow is high; new colors, families, shadows, and larger size changes are medium; smaller spacing/type/border changes are low. See src/drift/compare.ts for the complete rules.

Agent workflow

npx tether-ui check --agent > visual-qa.md

Paste the resulting Markdown into Codex, Claude Code, Cursor, or your preferred agent. It contains instructions to preserve behavior, followed by specific findings and expected values. No LLM calls, credentials, or network service is involved. Output ordering and IDs are stable for the same findings; scan timestamps are omitted from Markdown. Progress and ANSI styling are omitted from stdout in agent mode.

Terminal image created from actual tether output

Responsive checks

Defaults: 1440 × 1000, 1024 × 900, 768 × 900, 390 × 844. Each route/viewport gets an actual PNG capture. The Viewports screen links findings to their sizes and lets you enlarge or download each capture.

Intentional horizontal scroll containers and descendants clipped by their parents are excluded from off-screen checks. Text ellipsis and line clamps are not treated as accidental clipping. Screenshots show the viewport; style collection also covers rendered elements below the fold, up to the configured element cap.

Configuration

init creates tether.config.ts when one does not exist. Config is executable local code; load only configs you trust.

export default {
  url: 'http://localhost:3000',
  routes: ['/', '/settings'],
  viewports: [
    { name: 'desktop', width: 1440, height: 1000 },
    { name: 'laptop', width: 1024, height: 900 },
    { name: 'tablet', width: 768, height: 900 },
    { name: 'mobile', width: 390, height: 844 }
  ],
  minOccurrences: 2,
  maxElements: 5000,
  settleMs: 250,
  ignore: ['[data-tether-ignore]', '.live-clock']
};

--config path/to/config.ts selects another config. --url overrides its URL; query parameters apply to each configured route unless that route supplies its own value. Routes are origin-relative paths. A route or viewport change requires an explicit new baseline so coverage never silently shrinks.

.tether/
├── contract.json       # Editable tokens and distinct-element counts
├── baseline.json       # Original observations and scan configuration
├── report.json         # Latest findings, contract, scan, and agent brief
└── screenshots/        # Baseline and subsequent viewport captures

Add intentional values to contract.json to accept them globally, or use tether init --force to replace the baseline after review. tether never updates the baseline during check. Scans accumulate screenshot directories; remove unneeded scan directories when their reports are no longer needed.

For environments that cannot launch Chromium, TETHER_BROWSER_ENDPOINT=http://127.0.0.1:9333 connects to a dedicated CDP browser instead. Use an isolated profile, not your everyday browser. tether creates and closes its own contexts and disconnects after each scan. Standard installations need no endpoint setting.

CI

The frontend must already be running. Commit reviewed contract.json and baseline.json to compare against the same contract on every pull request. Recreate the baseline on the same OS/browser/font environment as CI to avoid platform differences.

# .tether is ignored by default. Add only reviewed, non-sensitive baseline files.
git add -f .tether/contract.json .tether/baseline.json
npx tether-ui check

Exit codes: 0 no drift, 1 drift found, 2 configuration or scanning failure. Agent mode uses the same codes. Do not generate a fresh baseline from each PR: that would approve the changes being tested.

A complete application workflow example starts the app, waits for readiness, checks the saved baseline, and uploads the report. This repository's CI workflow builds, tests, and runs the clean-to-drifted demo integration test.

Architecture

src/
  cli/          Commander commands, terminal and Markdown formatting
  scanner/      Playwright collection and browser lifecycle
  contract/     Frequency-based token inference
  drift/        Contract comparison, grouping, severity and score
  report/       Read-only loopback report server
  utils/        Config, normalization and JSON persistence
report/         Next.js + React + Tailwind + Lucide, exported at build time
demo/           Forma: one consistent UI, five deliberate CSS mutations
scripts/        End-to-end verification and real README asset generation
tests/          Deterministic core tests
docs/assets/    Composed hero and actual browser captures

The Next.js static export ships inside the npm package and reads local JSON at runtime. The CLI serves it on 127.0.0.1; users do not need a development server or a Next.js build. Scanner/browser setup follows Playwright's browser installation. No database, accounts, cloud backend, telemetry, or AI API.

Limitations

  • This checks computed styles and basic geometry, not pixel-perfect appearance, semantic design intent, accessibility, or image content.
  • No pseudo-elements, shadow DOM, iframe traversal, hover/focus states, authentication sessions, or interaction scripting yet. Lazy-rendered UI requires it to be present at scan time.
  • Screenshots use light mode with reduced motion. Browser defaults are filtered heuristically, not traced back to authored stylesheets.
  • Baseline selector stability matters. Large DOM restructuring and generated class changes can turn unchanged one-off values into findings.
  • Legitimate new design tokens still need human review. Font metrics, OS rendering, dynamic content, and CSS values beyond RGB/hex can affect results.
  • Large DOMs are capped and visibly marked as partial scans. A huge number of findings can reduce the heuristic score to zero.
  • Baselines and screenshots may contain private page content. They stay local, but review them before committing or sharing.

Roadmap

  1. Stable component identity and token aliases to reduce noise during refactors.
  2. Scripted interaction states and authenticated local scans.
  3. Baseline-to-current screenshot pairing with finding overlays.

Development and contributing locally

From a checkout of this repository:

npm install
npm run browser:install
npm run build
npm run demo

The forma demo opens at localhost:3000, with pastel glass panels over a cloud background. Leave it running. In a second terminal at the repository root, use the locally built CLI:

alias tether="node '$PWD/dist/cli/index.js'"

tether init
# Clean scan: exit 0.
tether check
tether report

If a baseline already exists, review it before replacing it with tether init --force.

Check the intentional drift

With a clean demo baseline saved:

# Drifted scan: exit 1, intentionally.
tether check --url 'http://localhost:3000/?drift=1'
tether check --agent --url 'http://localhost:3000/?drift=1'
tether report

The demo's Introduce drift link switches its visible state. The URL override above tells the scanner to inspect that state; switching a browser tab alone does not change the scan target. The clean and drifted URLs share the same route identity for comparison.

Development checks

npm run build
npm test
npm run test:e2e

The E2E test starts its own demo and report servers on available ports. Each run creates a fresh temporary baseline, checks the clean and drifted pages, verifies the report and agent output, then stops its servers and removes its temporary files. No manually started servers or existing .tether folder are needed.

See CONTRIBUTING.md. To regenerate the README images, run npm run assets.

License

MIT.

About

Visual linting for AI-coded interfaces. tether learns your UI’s existing design language, then catches drift as humans and agents make changes.

Topics

Resources

Contributing

Stars

1 star

Watchers

2 watching

Forks

Releases

Contributors

Languages