Inspect UI
The one rule: a visual claim is settled by rendering, never by inference.
A careful read of a style diff produces hypotheses — "these two classes may
collide", "this might overflow at phone width", "this pair looks low-contrast"
— and a hypothesis reported as a finding is a guess wearing evidence's
clothing. This skill turns each one into a verdict with a screenshot behind it.
0. Scope the surfaces
From the range: git diff <base>...HEAD --name-only, keep the files a browser
renders (HTML, CSS/styling, JSX/TSX/Vue/Svelte components, templates). None →
report no rendered surface and stop. Map each kept file to the screen(s) or
route(s) where its change shows. Done when: every changed rendered file has a
named screen.
1. Get it running — live first
Read docs/agents/project.md Run locally (dev) and start the app; missing
→ discover the command, confirm the app loads, and write it back (same
contract as validate-ui). Cannot run it (no browser, server broken) → the
review degrades, loudly: the report's first line says
cannot drive — static review only, and every visual conclusion below carries
the label unverified (static). Static reading is admissible only under that
label. Done when: the app is loaded in a browser, or the degraded mode is
declared.
2. Deterministic floor passes
Run before opening the browser, record the output:
- Off-token color:
grep -nE '#[0-9a-fA-F]{3,8}\b|rgba?\(' over the
changed style files. When the repo has a token system, a hit outside the
token-definition file(s) is an Important finding; no token system → note
that instead.
- Suppressed focus:
grep -nE 'outline: *(none|0)' over the changed
styles. A hit with no visible replacement (:focus-visible rule, custom
outline/box-shadow) in the same diff is an Important finding.
These are the floor, not the review. Done when: both passes ran and their
hits are recorded.
3. Hold the contract
What does "right" look like, in precedence order: the feature's design.md
## UI design section (its States: lines are the case list for step 4, its
tokens the palette of record) → an Approved docs/standards/design-tokens.md
(stack, token vocabulary, numeric floors, forbidden patterns — its floors set
severity) → else the repo's token/theme file plus the visual language of the
screens around the change. Name which contract you hold. Done when: the contract source is named in the report.
4. Drive, capture, judge
Per changed surface, in the running app (the repo's e2e harness, a browser MCP
tool, or npx playwright screenshot). Screenshots land under
.skills/<CODE>/inspect-ui/ (no CODE → .skills/inspect-ui/<branch>/); the
working tree stays untouched.
- Viewports — 1440×900, 768, 375. At each: screenshot, and check
document.scrollingElement.scrollWidth <= window.innerWidth. Horizontal
overflow, clipping, or overlap at any of the three is Important. Spec
silence does not downgrade it: the requirement is the page, and the rest of
the page reflows — a surface that breaks at a common width is broken.
- States. Exercise every reachable state: hover, keyboard focus (Tab to
each new interactive element and screenshot — is focus visible?),
selected/active, disabled, empty, error — and every composed state the
code makes possible (two classes on one element, emphasis while selected).
The collision a static read can only call "fragile" is settled here by
producing it and looking.
- Contract walk. Each
States: line in the UI design section gets
exercised; a state the contract names but the screen cannot reach — or
reaches looking wrong — is a finding. Colors on screen trace to tokens.
- Contrast. For each new text/background pair, compute the WCAG ratio
from the actual rendered values. Defaults: body text below 4.5:1 is
Important; below 1.5:1 the content is effectively invisible — Critical. An
Approved
design-tokens.md's floors override these defaults.
- Themes. WHEN the app's styles define a second theme
(
prefers-color-scheme media query or a data-theme selector) → capture
each changed surface in both themes; a token redefined in one theme but not
the other, or a new raw value that ignores the theme split, is a finding.
No second theme in the styles → skip, note once.
Read every screenshot you capture. Done when: every changed surface has its
three viewport shots plus one per exercised state, all read.
5. Report — fixed shape
- One block per finding: severity (Critical / Important / Minor),
file:line,
screenshot path, why it matters, the fix unless obvious. A visual
finding with no screenshot path and no unverified (static) label is not
done — go back and capture it.
needs-human-eyes: — the genuine taste calls the running app cannot settle
(does this feel right for the brand? is this treatment too loud?). Write
none when empty; this line feeds the product-walk predicate in
execute-common.
- Verdict line:
UI: clean | findings | cannot drive (static only).
Rationalizations
| Thought |
Reality |
| "The CSS read already shows the bug — no need to run it" |
A read is a hypothesis. The screenshot settles it, and catches what reading can't: cascade order, inheritance, real widths |
| "No responsive requirement in scope — overflow at 375 is Minor" |
The rest of the page reflows. Broken at a common width is Important, banked nowhere |
| "Tests are green, so it renders fine" |
The tests assert strings and DOM state, not pixels. The invisible-text bug passes a .match() test |
| "Both classes probably compose — different properties" |
Produce the composed state and look. Declaration order is not a verdict |
| "Screenshot captured — attach and move on" |
An unread screenshot is not evidence. Read it, then judge |
Red Flags
- A visual verdict with no screenshot path and no
unverified (static) label
- Skipping the 375px viewport
- Judging a composed state from declaration order instead of producing it
outline: none passing because "the design never mentioned focus"
- Mutating the working tree, index, or branch state during review
1---2name: inspect-ui3description: Use when a diff or branch touching browser-rendered surfaces (HTML, CSS/styling, JSX/TSX/Vue/Svelte components, templates) needs its visual and interaction quality judged in the real running app before merge — the live design review invoked by `inspect-change`'s UI lane, or when the user asks to design-review a change, check how it looks, responsive breakage at mobile widths, focus visibility, contrast, dark mode, or off-token styling. Produces a screenshot-backed UI finding set across desktop/tablet/mobile viewports, interaction and composed states, and design-contract conformance — settling cascade and severity questions by driving the app, never by inference from the diff. Not for asserting spec'd behavior e2e (validate-ui), authoring a human product walk (write-flow-guide), or styling a standalone page (craft-page).4---56# Inspect UI78The one rule: **a visual claim is settled by rendering, never by inference.**9A careful read of a style diff produces hypotheses — "these two classes may10collide", "this might overflow at phone width", "this pair looks low-contrast"11— and a hypothesis reported as a finding is a guess wearing evidence's12clothing. This skill turns each one into a verdict with a screenshot behind it.1314## 0. Scope the surfaces1516From the range: `git diff <base>...HEAD --name-only`, keep the files a browser17renders (HTML, CSS/styling, JSX/TSX/Vue/Svelte components, templates). None →18report `no rendered surface` and stop. Map each kept file to the screen(s) or19route(s) where its change shows. *Done when: every changed rendered file has a20named screen.*2122## 1. Get it running — live first2324Read `docs/agents/project.md` **Run locally (dev)** and start the app; missing25→ discover the command, confirm the app loads, and write it back (same26contract as `validate-ui`). Cannot run it (no browser, server broken) → the27review **degrades, loudly**: the report's first line says28`cannot drive — static review only`, and every visual conclusion below carries29the label `unverified (static)`. Static reading is admissible only under that30label. *Done when: the app is loaded in a browser, or the degraded mode is31declared.*3233## 2. Deterministic floor passes3435Run before opening the browser, record the output:3637- **Off-token color:** `grep -nE '#[0-9a-fA-F]{3,8}\b|rgba?\('` over the38 changed style files. When the repo has a token system, a hit outside the39 token-definition file(s) is an Important finding; no token system → note40 that instead.41- **Suppressed focus:** `grep -nE 'outline: *(none|0)'` over the changed42 styles. A hit with no visible replacement (`:focus-visible` rule, custom43 outline/box-shadow) in the same diff is an Important finding.4445These are the floor, not the review. *Done when: both passes ran and their46hits are recorded.*4748## 3. Hold the contract4950What does "right" look like, in precedence order: the feature's `design.md`51`## UI design` section (its `States:` lines are the case list for step 4, its52tokens the palette of record) → an Approved `docs/standards/design-tokens.md`53(stack, token vocabulary, numeric floors, forbidden patterns — its floors set54severity) → else the repo's token/theme file plus the visual language of the55screens around the change. Name which contract you hold. *Done when: the contract source is named in the report.*5657## 4. Drive, capture, judge5859Per changed surface, in the running app (the repo's e2e harness, a browser MCP60tool, or `npx playwright screenshot`). Screenshots land under61`.skills/<CODE>/inspect-ui/` (no CODE → `.skills/inspect-ui/<branch>/`); the62working tree stays untouched.6364- **Viewports — 1440×900, 768, 375.** At each: screenshot, and check65 `document.scrollingElement.scrollWidth <= window.innerWidth`. Horizontal66 overflow, clipping, or overlap at any of the three is **Important**. Spec67 silence does not downgrade it: the requirement is the page, and the rest of68 the page reflows — a surface that breaks at a common width is broken.69- **States.** Exercise every reachable state: hover, keyboard focus (Tab to70 each new interactive element and screenshot — is focus visible?),71 selected/active, disabled, empty, error — and every **composed** state the72 code makes possible (two classes on one element, emphasis while selected).73 The collision a static read can only call "fragile" is settled here by74 producing it and looking.75- **Contract walk.** Each `States:` line in the UI design section gets76 exercised; a state the contract names but the screen cannot reach — or77 reaches looking wrong — is a finding. Colors on screen trace to tokens.78- **Contrast.** For each new text/background pair, compute the WCAG ratio79 from the actual rendered values. Defaults: body text below 4.5:1 is80 Important; below 1.5:1 the content is effectively invisible — Critical. An81 Approved `design-tokens.md`'s floors override these defaults.82- **Themes.** WHEN the app's styles define a second theme83 (`prefers-color-scheme` media query or a `data-theme` selector) → capture84 each changed surface in both themes; a token redefined in one theme but not85 the other, or a new raw value that ignores the theme split, is a finding.86 No second theme in the styles → skip, note once.8788Read every screenshot you capture. *Done when: every changed surface has its89three viewport shots plus one per exercised state, all read.*9091## 5. Report — fixed shape9293- One block per finding: severity (Critical / Important / Minor), `file:line`,94 **screenshot path**, why it matters, the fix unless obvious. A visual95 finding with no screenshot path and no `unverified (static)` label is not96 done — go back and capture it.97- `needs-human-eyes:` — the genuine taste calls the running app cannot settle98 (does this feel right for the brand? is this treatment too loud?). Write99 `none` when empty; this line feeds the product-walk predicate in100 `execute-common`.101- Verdict line: `UI: clean | findings | cannot drive (static only)`.102103## Rationalizations104105| Thought | Reality |106|---|---|107| "The CSS read already shows the bug — no need to run it" | A read is a hypothesis. The screenshot settles it, and catches what reading can't: cascade order, inheritance, real widths |108| "No responsive requirement in scope — overflow at 375 is Minor" | The rest of the page reflows. Broken at a common width is Important, banked nowhere |109| "Tests are green, so it renders fine" | The tests assert strings and DOM state, not pixels. The invisible-text bug passes a `.match()` test |110| "Both classes probably compose — different properties" | Produce the composed state and look. Declaration order is not a verdict |111| "Screenshot captured — attach and move on" | An unread screenshot is not evidence. Read it, then judge |112113## Red Flags114115- A visual verdict with no screenshot path and no `unverified (static)` label116- Skipping the 375px viewport117- Judging a composed state from declaration order instead of producing it118- `outline: none` passing because "the design never mentioned focus"119- Mutating the working tree, index, or branch state during review