AI agent skill · by @lobehub
Ux Audit
Audit a page / surface against the Designing Interfaces pattern language + the ux skill checklists, then land findings. Three layers — static (code), visual (screenshots), dynamic (automated user journey + perf). Use to run a repeatable, standards-based UX review of one screen.
Archived 2026-10-23 · Source updated 2026-10-01. Check the original repository for the latest version.
SKILL.md instructions
# UX Audit
A repeatable, standards-based UX review of **one surface at a time**. The benchmark is
two things together:
1. **Jenifer Tidwell, _Designing Interfaces_** — the pattern language for what a good
interface is _made of_. See [`references/pattern-catalog.md`](references/pattern-catalog.md).
2. **The [`ux`](../ux/SKILL.md) skill** — LobeHub's execution checklists for how a flow
should _behave_.
The audit answers: **which patterns does the surface use** (and how well), and **where is
the experience weak** (each gap tied to a checklist item). Recurring gaps feed back as new
`ux` checklist items; the audit itself becomes a worked-example reference.
Do **one surface per run** — a full-app sweep is too much for a single pass. Re-run per
page as the product grows; that's the "continuous" part.
## Three layers — pick by what you need to catch
An audit is not one activity. A finding is only trustworthy from a layer that can actually
_see_ it. Each layer has its own procedure file; run the ones the surface needs.
| Layer | File | What it does | Catches | Cost |
| -------------- | --------------------------------------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- |
| **L1 Static** | [layer-1-static.md](references/layer-1-static.md) | Read the code | Missing states/branches (empty/error/retry), no draft persist, absent patterns, structural issues | cheap, offline, **every audit** |
| **L2 Visual** | [layer-2-visual.md](references/layer-2-visual.md) | Screenshots of the rendered surface | Real visual hierarchy & dominant control, spacing/contrast/alignment, truncation/overflow, how empty/loading/error actually look, responsive breakpoints, dark/light | medium; needs a render |
| **L3 Dynamic** | [layer-3-dynamic.md](references/layer-3-dynamic.md) | Drive the real user journey via **acceptance** + instrument | In-progress/locked states, forced error/empty states, does step N lead to N+1, focus/keyboard, **quantified CLS / LCP / INP / long-tasks** | high; needs a running env + auth |
### Coverage matrix — which layer can conclude what
The core rule: **a verdict must come from a layer that can see it.** Don't tick a visual or
runtime verdict off the code.
| Finding type | L1 | L2 | L3 |
| ------------------------------------------------------------------------- | :---------: | :--------------: | :-------------: |
| Missing empty/error branch, no retry, draft not persisted, absent pattern | ✅ | — | — |
| Real visual hierarchy / is the dominant control the primary action | ❌ misleads | ✅ | ✅ |
| Spacing / alignment / contrast / truncation / overflow / dark mode | ❌ | ✅ | — |
| Off-screen selection; what empty/loading/error actually render as | ❌ | ✅ | ✅ |
| Responsive breakpoints (narrow / mobile) | ❌ | ✅ | ✅ |
| In-progress / locked states; forced error / empty; capability-gated | ❌ | ❌ | ✅ |
| Journey stitching (forward momentum across steps) | weak | weak | ✅ |
| Focus order / keyboard reachability | ❌ | ❌ | ✅ |
| **CLS / LCP / INP / long-task numbers** | ❌ | qualitative only | ✅ |
| **Which of two variants is _better_ (A/B winner)** | ❌ misleads | ❌ misleads | ✅ (+analytics) |
> ⚠️ The recurring trap this prevents: ticking "one primary button" or "empty is a real
> page" from a `variant` prop in the code. Those are **L2** verdicts — confirm them on the
> render, never from L1 alone.
### Tiering — don't run all three every time
- **L1 always** — fast, complete-coverage baseline for every surface.
- **Add L2** when the findings are about layout, hierarchy, rendered states, or responsive.
- **Add L3** when you need to walk a journey, force states L1/L2 can't reach, or measure
performance (CLS etc.).
`--l1 / --l2 / --l3` scopes a run to one layer; default is L1 (+ L2 if screenshots are
supplied).
## Ground rule: evidence, not vibes
Every finding cites its evidence — `file:line` (L1), a screenshot you **verified with the
Read tool** (L2), or a captured value / snapshot (L3). Before asserting a load-bearing
claim, confirm it in the layer that owns it; a wrong "it's missing" is worse than no
finding.
## Ground rule: benchmark the surface _class_, not just our own artifact
Reading our code can only surface flaws in **what we built** — it is structurally blind to a
capability we **never built at all**, because an entirely-absent affordance leaves no
`file:line`, no dead branch, no half-wired button to grep for. The checklists guard the
_quality of the states that exist_; they do **not** tell you which states a surface of this
_class_ is expected to have.
So before (or alongside) reading code, name the surface's **class** and its domain
conventions: how do the mature, comparable products build this exact screen, and what do
they offer that a first version forgets? An **OAuth consent** screen's class norms, for
example (GitHub / Google / Okta): show _which identity_ you're authorizing as **and let the
user switch account / re-authenticate**, name the requesting app, list the scopes, allow
deny, and point to later revocation. A **file picker**, a **checkout**, a **share dialog**
each carry their own class norms. Write this expected-capability list _first_, then audit
gaps against it — otherwise the audit only ever polishes the paths that already exist and
silently blesses a missing one.
> ❌ The first pass of the OAuth audit measured consent against our internal state
> checklists only and reported button-hierarchy / retry gaps, while missing the **biggest**
> one: the consent screen locks the user into the current identity with **no switch-account**
> path (`OAuthConsent/Login.tsx`) — a class norm every comparable OAuth provider ships. A
> competitor-norms pass catches this on minute one; a code-only pass never can.
## Ground rule: comparing two variants — the winner is an outcome verdict, not a craft verdict
When an audit compares **two variants of the same surface** ("is Agent or Classic onboarding
better?"), the trap is judging **which is better _made_** (more polished, more patterns, more
AI) when the real question is **which better gets the user to their goal**. For a
gateway / interstitial surface — onboarding, consent, paywall, a loading gate — the two
diverge hard: the best version is often the _least_ version, because the surface stands
_between_ the user and what they came for. Craft is not outcome, and the richer artifact is
routinely the worse one.
So a variant comparison must:
1. **Name the success metric _first_, then judge against it.** Onboarding = completion rate +
time-to-value + drop-off, not pattern richness. A checkout = conversion. Write the metric
before scoring, or you'll default to scoring craft.
2. **Gate the winner verdict on L3 / analytics.** "Which variant is better" is a behavioral
outcome — it lives in the coverage matrix's L3 row alongside CLS/INP. From L1/L2 you may
compare **mechanics** ("A's error recovery is more complete", "B is fewer steps"); you may
**not** declare a winner. No funnel data → say "insufficient evidence, here's what I'd
need", and stop. A confident winner call with a buried "needs L3" caveat is the failure —
the caveat does not license the verdict.
3. **Cost to the user is a first-class axis, inverse-weighted for gateway surfaces.** Time,
steps, tokens, latency. Past a threshold, richness is a _liability_ on any surface the user
wants to get _through_, not _into_.
4. **Anchor 意义感 on the user's real goal, not the feature's richness.** A flow that detains
the user in itself when their goal is elsewhere is _less_ Meaningful even if more engaging —
read correctly, 意义感 and 自然 favor the fast path there. Don't mistake "more conversational
/more AI /more crafted"for"more meaningful".
5. **Read the org's revealed preference as evidence.** Feature flags, which variant is the
**fallback**, which the most-constrained platform is forced onto (desktop), recent reverts.
When the universal fallback _is_ variant B, B is the trusted baseline and the burden of
proof is on A — don't explain these signals away as "ceiling vs floor".
6. **Pick the right reference class, and weight by the real intent distribution.** An AI
tool's first-run benchmarks against ChatGPT / Claude / Cursor (near-zero onboarding, straight
to the box), not SaaS setup wizards (Notion / Linear) that reward thorough onboarding.
Score the modal user (who wants to skip), not the ideal engaged one.
> ❌ This skill's own miss: an L1 read judged **Agent onboarding "better" than Classic** because
> it was more polished /conversational (richer completion panel, name suggestions, view
> transitions), citing 意义感 ≳ 自然 > 确定性. In production Agent's effective-guidance completion
> was **not high**, users found it too slow (they wanted the tool, not a chat), and the org
> **rolled back to Classic**. Every error above was present: craft mistaken for outcome, 意义感 scored
> backwards, cost footnoted, a winner declared from L1 on what is an L3/analytics metric, and the
> flag-gated /degrades-to-Classic/desktop-excluded signals explained away.
## Ground rule: a "redundant" control is a composition question before a subtraction one
When L1 spots two controls that seem to do the same thing, the reflex is **subtraction** —
delete one, hide one, merge them, differentiate the copy. Resist it. Two controls that share
an _intent_ often differ in **scope**, and the honest fix expresses that scope difference in
**layout** — promote the wider-scope one to a visible, titled **sibling** — not by removing
it. Subtraction is a _behavior_-layer move; the better answer usually lives in the
**composition** layer, which is precisely the half of the benchmark an audit drops when it
walks the [`ux`](../ux/SKILL.md) checklists but never opens
[`references/pattern-catalog.md`](references/pattern-catalog.md). The checklists speak states,
momentum, and draft-safety; they carry no vocabulary for _Titled Sections / Grid of Equals /
Center Stage_, so a checklist-only read reaches for "dedupe" every time. **Walk the
pattern-catalog pass (L1 step 2) before writing a remedy for any "redundant / overlapping"
finding**, and ask: _same intent, different scope?_ → the fix is a sibling, not a delete.
> ❌ The CC AskUserQuestion audit flagged the per-question "write your own" box and the global
> "Or type directly" escape as redundant and proposed hiding / merging them — a subtraction.
> They aren't redundant: the per-question box is _question_-scoped, the escape is
> _whole-form_-scoped. The composition-layer answer is to hide the escape when there's one
> question and render it as a **visible peer** to the question block when there are several — a
> _Titled Sections_ move a checklist-only read never surfaced. Skipping the catalog cost the
> better answer.
## Ground rule: report the good, not only the gaps
An audit that only lists what's broken has drifted into a bug report. The mandate is
**patterns in use _and how well_** — so the strengths are first-class findings, not table
decoration. A well-built state machine, a draft that survives a failed save, an
open-redirect guard, a smart default: these are the **good cases**. Name them, cite their
`file:line`, and mark the standouts **✅ 亮点**, for three reasons:
- **They teach.** A good case is the ✅ half of the 回灌 loop — it becomes the positive
example a `ux` checklist item cites, exactly as a gap becomes the ❌ one. An audit that
reports no good cases can only ever sharpen the ❌ side of the checklists.
- **They protect.** "Don't regress this" is a finding. The next refactor needs to know
which behaviors are load-bearing — the **Strengths worth preserving** block in
[`example/task-detail.md`](references/example/task-detail.md) (the loading/not-found state
machine, comment-draft-preserved-on-failure, the run-all preview → confirm flow) and the **✅ 亮点**
rows in [`example/home.md`](references/example/home.md) (empty-send → day's-hint fallback,
drawer-not-navigate surface contract) are exactly that list.
- **They calibrate severity.** A gap reads differently against a surface that is otherwise
strong than against one weak throughout; the good cases are the baseline the gaps rank on.
> ❌ The drift this prevents: an audit whose "Patterns in use" ✅ rows are one-word ticks
> ("persistent", "strong", "textbook") while every gap gets a paragraph — the good cases
> demoted to checkboxes. [`example/task-detail.md`](references/example/task-detail.md) is the model
> to match: each standout pattern carries its `file:line` and a **✅ 亮点** call-out, so the
> strengths are as legible as the gaps.
## Severity rubric (shared)
- 🔴 **Breaks trust** — data / input loss, stuck / permanent states, a misleading "empty"
that hides a failure, silent send failure.
- 🟠 **Dead-ends or misleads** — no forward path, ambiguous state, missing in-progress
feedback, an empty state that isn't a real page.
- 🟡 **Friction / inconsistency / missed delight** — predictability, redundant controls,
progressive-disclosure gaps, CLS jank. This tier is the **easiest to under-report**: a pass
hunting correctness skews 🔴/🟠 and glides past micro-consistency — sibling elements styled
differently (options carry a number chip, the free-text row doesn't), an affordance with no
label. When the surface is otherwise solid, deliberately switch into the **interface-details**
lens for one pass, or these never get written down.
## Output (shared)
See the worked example, [`references/example/home.md`](references/example/home.md). Note
**which layers ran**, then:
1. **Patterns in use** — table (from L1/L2), grouped by pattern family, with a one-line
read; mark each standout ✅ row **亮点** and back it with real evidence (`file:line`),
never a one-word tick.
2. **Strengths / good cases (don't regress)** — a **dedicated section** (`## … — Strengths /
good cases`), not a table footnote: bulleted **✅ 亮点** items, each naming the behavior
this surface gets _right_, its evidence (`file:line`), and _why_ it's load-bearing — the
✅ half of the 回灌 loop and the "don't regress" list for the next refactor. Flag the ones
strong enough to land as **✅ examples** in `ux` (annotate `→ landed as ux <ref> ✅`, wired
to the Skill-feedback section). **Every audit produces this section** — if a surface is
genuinely weak throughout, say that explicitly rather than omit it. See
[`example/fleet.md`](references/example/fleet.md) §2 for the shape.
3. **Experience gaps** — ranked; each names the finding, the `ux` checklist item / catalog
pattern it violates, the **layer + evidence** it came from, and a one-line remedy.
4. **Skill feedback** — real instances of existing checklist items vs new generalizable gaps
worth adding to `ux`, **and** good cases worth landing as ✅ examples (see 回灌 below).
## Land the findings (shared)
An audit is not finished when the findings are written — it is finished when they are
**landed**. All three steps below are **required** to close a run:
- **Concrete bugs** → fix the top 🔴, or file as Linear sub-issues under the "UX Audit"
parent (per-page container issue → one sub-issue per finding).
- **Generalizable gaps → 回灌 `ux` (mandatory).** Every run **must** close the loop back into
the `ux` skill: for each finding that generalizes beyond this surface, add / strengthen a
`ux` checklist item (rule + ✅/❌ example in the right module), citing the audited surface
as the ❌ example. Add or update a short cue in the ux Quick review only when its existing
cues do not cover the requirement; keep examples and implementation details in the module.
This is what makes the audit
_continuous_ — each run leaves the checklists sharper than it found them. If a run genuinely
surfaces **no** generalizable gap, say so explicitly in the report's Skill-feedback section
(only validated-existing-rule instances) — silence is not an acceptable close.
- **Exemplary good cases → 回灌 `ux`, refining the rule (not just decorating it).** The 回灌
loop has two halves: a gap sharpens a checklist item's ❌ example, a good case sharpens its ✅
one. But a good case is only worth landing if it **teaches the rule something** — the ✅
example is the floor; **the goal is to sharpen the rule text or extract a latent sub-rule /
checklist item** the current rule doesn't yet state. So when an audit finds a pattern done
notably well, ask: _what technique does this do that the rule doesn't already demand?_ and
fold that back — refine the prose, split a one-trigger rule into its real cases, add a
checklist line — citing the surface as the ✅ example. A good case that merely re-illustrates
an already-complete rule adds little; a good case that reveals a missing distinction is the
point. (e.g. Fleet's scroll-into-view extracted "the re-run trigger has two flavors — async
arrival vs. imperative-add-then-paint" and "the scroll axis follows the list direction" into
Read §1.3; its skeleton extracted "match the text's **width proportion**, not just height"
into Feedback §4.1 — neither was stated before.)
- **The audit** → save it as `references/example/<page>.md` so the next run has a template.
> The audit and the `ux` skill are a **closed loop**: `ux` is the benchmark the audit measures
> against, and the audit is the mechanism that keeps `ux` honest. Skipping the 回灌 breaks the
> loop and reduces the audit to a one-off review.
## Related skills
- **[ux](../ux/SKILL.md)** — the execution checklists this audit measures against, and where
generalizable findings get landed.
- **acceptance** — the automation framework L3 drives (agent-browser CDP: snapshot / eval
/ screenshot / GIF). L3 assumes its Step 0 env + auth are green.
- **deep-review** — code-level review; this skill is its design-level sibling (deep-review's `ux` dimension checks flows inside a code review; this skill audits a whole surface).
- **skills-audit** — the same "periodic, evidence-based audit" shape, applied to the skill
catalog.