Skip to content

Quality bar

The ship gate. A component is not stable until every box is checked.

Sections 1–3 are adapted from ibelick/ui-skills (baseline-ui, fixing-motion-performance, fixing-accessibility), section 7 from addyosmani/web-quality-skills (core-web-vitals). Sections 4–6 are ours.


1. Baseline UI

Stack

  • Tailwind defaults unless a token exists or a deviation is explicitly justified
  • motion/react when JS animation is genuinely required
  • tw-animate-css for entrance micro-animations
  • cn() (clsx + tailwind-merge) for all class logic

Components

  • Accessible primitives (Base UI preferred, Radix accepted) for anything with keyboard or focus behavior
  • Existing project primitives used before new ones are created
  • Never two primitive systems in one interaction surface
  • aria-label on every icon-only button
  • Keyboard and focus behavior never hand-rolled

Interaction

  • AlertDialog for destructive or irreversible actions
  • Structural skeletons for loading
  • h-dvh, never h-screen
  • env(safe-area-inset-*) on every fixed element
  • Errors shown next to where the action happened
  • Paste never blocked

Animation

  • No animation that isn't specified in the motion catalog
  • Only transform and opacity animated
  • No layout properties animated (width, height, top, left, margin, padding)
  • Paint properties only on small, isolated surfaces
  • ease-out on entrance
  • Nothing over 300ms for interaction feedback
  • Looping animations pause off-screen
  • prefers-reduced-motion respected
  • Only tokenized easing curves — no inline cubic-bezier

Typography

  • text-balance on headings, text-pretty on body
  • tabular-nums on all data
  • truncate / line-clamp in dense UI — never on a number
  • tracking-* untouched outside the scale

Layout

  • Fixed z-index scale, no arbitrary z-*
  • size-* for square elements, not w-* h-*

Performance

  • No large blur() or backdrop-filter animation
  • will-change only during an active animation
  • No useEffect for anything expressible as render logic

Design

  • No gradients (except a single-hue area-chart fill)
  • No purple, no multicolor
  • No glow as a primary affordance
  • Empty states have exactly one clear next action
  • One accent per view
  • Existing tokens used before new ones are introduced

2. Motion performance

Never patterns

  • No interleaved layout reads and writes in a frame
  • No continuous layout animation on a large surface
  • No animation driven by scrollTop / scrollY / scroll events
  • No requestAnimationFrame loop without a stop condition
  • No two animation systems mutating the same element's layout

Mechanism

  • transform / opacity by default
  • JS animation only where interaction requires interruption
  • Paint/layout animation only on small, isolated surfaces
  • One-shot preferred over continuous

Measurement

  • Measure once, then animate with transform
  • All DOM reads batched before writes
  • FLIP for layout-like effects

Scroll

  • Scroll/View Timelines or IntersectionObserver, never scroll polling
  • Animations pause off-screen

Paint, layers, blur

  • No paint animation on a large container
  • No animated CSS variables for transform/opacity/position
  • No inherited animated CSS variables
  • will-change surgical and temporary
  • Blur ≤ 8px, one-shot, small surface only

3. Accessibility

  • Every control has an accessible name
  • Everything interactive is Tab-reachable
  • Focus is always visible; no removed outline without a replacement
  • No tabindex > 0
  • Escape closes overlays
  • Dialogs trap focus; focus returns to the trigger on close
  • Native elements over roles; required ARIA present where roles are used
  • Heading levels don't skip
  • <th scope> on every data table
  • Errors linked with aria-describedby; aria-invalid set
  • Disabled submit explains why
  • aria-live on critical errors and live data (throttled)
  • aria-expanded + aria-controls on disclosures
  • Contrast passes — 7:1 for monetary values
  • Hover-only interactions have keyboard and touch equivalents
  • Disabled state doesn't rely on color alone
  • Correct alt (empty for decorative illustration)

4. Financial correctness

The section no generic checklist has.

  • Money stored in minor units — no floats anywhere in the path
  • Decimal places resolved from the currency, not hard-coded
  • Negative renders correctly: parentheses in statements, − (U+2212) in deltas
  • Negative values don't break decimal alignment — paren column reserved
  • zero / null / n/a / redacted / loading are five distinct renders
  • polarity respected — up is not assumed good
  • Sign readable in greyscale ← the ship gate
  • No abbreviation in tables, statements, totals, inputs, receipts
  • Abbreviated values expose the exact value on hover/tooltip
  • Percentage points vs. percent explicitly discriminated
  • Division by zero → n/a, never NaN / Infinity / 100%
  • Rounded parts that must sum use largest-remainder allocation
  • Multi-currency never silently summed
  • Converted values marked (≈) with rate and timestamp
  • Every figure has an "as of"
  • FX rates carry a timestamp; transactable rates carry a lock
  • Fiscal periods respected — January is not assumed
  • Period labels state resolved dates
  • Statement rule lines correct: single above subtotals, double below totals, numeric column only
  • Copy-to-clipboard yields the raw value, not the formatted string
  • No monetary amount in any analytics event or log
  • Screen reader announces the sign on negative values

4b. Accounting correctness

For anything in catalog families 9–13. Reviewed against docs/domain/ clause by clause.

  • Debit and credit are two columns; the balance check is displayed
  • No delete path on a posted entry — the action is absent, not disabled
  • Reversal creates a linked offsetting entry; both remain visible
  • A reversal in a closed period is dated in the current open period
  • The ledger sign is never negated at render time
  • Contra accounts render in natural position, netting to a subtotal
  • Non-postable accounts are blocked in every picker; normal balance shown
  • requiredDimensions enforced from the account; entity always required
  • Period state respected; cutoff warnings name both remedies
  • Roll-forward asserts opening = prior closing and closing = GL
  • Every BS account has a tie-out; unsupported renders explicitly
  • Accruals default to auto-reverse, with the reversal date shown
  • Schedules preview before posting; posted as one audited batch
  • Subledger totals tie to control accounts (AR, AP, fixed assets)
  • Aging is by due date; credit balances shown, not hidden
  • Unapplied cash is never revenue; a customer's net credit balance is flagged as a liability
  • Credit note, refund, and write-off are distinct actions
  • Every cash-application difference requires an explicit disposition
  • Duplicate bill detection is blocking and runs before save
  • Three-way match compares line by line, not header totals
  • Segregation of duties enforced — preparer cannot approve
  • Approvals bind to a version; edits void them and show the change
  • Evidence required on bills, expenses, adjusting entries, reconciliations
  • Vendor bank details versioned; changes flagged before payment runs
  • Payment runs show resulting cash and use hold-to-confirm
  • Statements carry the full header (entity, period, basis, framework, currency, scale, provenance)
  • Rule lines span the numeric column only; double rule under grand totals
  • Statement articulations asserted: NI → RE, cash flow → BS cash, A = L + E
  • Realized and unrealized FX are separate lines
  • Only monetary items revalued; translation → equity, remeasurement → P&L
  • Missing FX rate blocks; rate type required
  • Intercompany matrix nets to zero; break types named
  • Materiality: both thresholds required, threshold displayed
  • Non-GAAP measures reconciled, adjustments named
  • Every metric ships a MetricDefinition
  • Audit trail records field-level diffs with server UTC time and actor

5. Design system conformance

  • All spacing on the 8px grid (4px only inside ≤32px controls; table cells the one documented exception)
  • Radius from the scale; nesting formula r_inner = r_outer − gap satisfied
  • Max 3 font sizes, max 3 font weights — counted in the spec
  • Two families only (Inter, Geist Mono); the display family only on a solitary non-aligning figure
  • tnum + case on every numeric element
  • Currency symbol subordinated to the value
  • Resting surfaces use a border, not a shadow
  • Shadows ≥ 2 layers, hue-tinted, only on floating surfaces
  • Dark mode uses the top highlight
  • Icons at 16/20/24, stroke 1.5, optically aligned
  • Third-party logos contained at radius 8; avatars circular
  • Gaps from the proximity ladder only
  • Every token defined for both themes
  • Component owns padding, never margin
  • Enum props, not booleans, for visual variants
  • forwardRef + displayName; named export; className merged via cn()

6. Edge cases

  • Negative · zero · null · redacted
  • Very large value · very long label
  • JPY (0dp) · KWD (3dp) · multi-currency
  • RTL
  • Loading · error · empty · offline/stale
  • 320px · 390px · 768px · 1024px · 1280px
  • 200% and 400% zoom
  • Reduced motion · forced colors · signal palette · greyscale
  • Light · dark
  • Touch · mouse · keyboard-only · screen reader
  • Real iPhone SE · real mid-range Android · 13" MacBook at 125%
  • 4× CPU throttle · Fast 3G
  • Print

7. Field performance

Sections 1 and 2 govern the frame. This one governs the field — what the user's machine actually delivers.

  • INP under 200ms on every interaction the component owns — measured, not assumed
  • Measured on a mid-range Android at 4× CPU throttle, not on the machine that wrote the code
  • No single event handler over 50ms; long work broken up or moved off the main thread
  • Filter, sort and search yield to paint before doing the work — the input reflects the keystroke first, the rows follow
  • Virtualization above ~200 rows, with the virtualizer measured under throttle rather than trusted
  • CLS zero on data load — the skeleton reserves the exact final height, including the numeric column width
  • Font swap shifts nothing: size-adjust fallback metrics set
  • The LCP element is identified per page, and it is not a chart
  • A value updating shifts nothing — tabular-nums plus the reserved paren column (numerics)

Why INP and not frame rate. A table can hold a clean 60fps while taking 400ms to acknowledge a click, and the second number is the one the user feels. Sections 1–2 are about the frames during a transition; INP is about the delay before one starts. A component can pass every box above and still fail here.

The finance-specific trap: filtering ten thousand rows synchronously inside an onChange. It never surfaces in a motion review, because nothing is animating — the interface is simply frozen, and frozen has no frames to step through. Yield first, filter second.


The deslop list

Specific markers of generic, machine-generated UI. Any one of these fails review on sight.

Tell Fix
transition: all Name the properties
scale(0) entrance scale(0.97) + opacity: 0
ease-in on a UI element --ease-out
20–40px entrance offset 8px
transform-origin: center on a popover var(--transform-origin)
Animation on a keyboard action Remove it
Duration > 300ms on interaction 140–240ms
Hover effect with no @media (hover: hover) Gate it
Keyframes on a rapidly re-triggered element Transitions
Same enter and exit duration Exit = enter × 0.7
Everything appearing at once Stagger 40ms, cap 240ms
shadow-md on a resting card border: 1px solid var(--border)
Purple / multicolor gradient One accent, no gradient
Glow as an affordance Border, weight, or position
Emoji in a UI label An icon, or nothing
Uppercase table headers label at --text-secondary
Centered numbers Right-aligned, tabular-nums
toFixed(2) Intl.NumberFormat
value ?? 0 Three explicit branches
Count-up number animation Flash the background, swap the digits
Chart re-animating on filter change First paint only
Shimmer sweep skeleton Structural skeleton, slow breathe
h-screen h-dvh
w-6 h-6 size-6
Arbitrary z-[999] The z scale
tracking-tight on body text Nothing
5 font sizes in one card 3
"Oops! Something went wrong 😅" "Couldn't load transactions."
A tooltip carrying essential information Inline it
One signed amount column for a ledger Two columns: debit, credit
A delete button on a posted entry Remove it. Offer reverse.
Accumulated depreciation as a positive asset Contra, indented, parenthesized, netting to a subtotal
Aging computed from the invoice date Aging is from the due date
Unapplied cash booked to revenue, or a customer's net credit left inside AR Shown separately within AR; a net credit is reclassified to a liability
"Balance" with no qualifier Available or current — say which
A metric with no definition MetricDefinition, or don't ship the metric
EBITDA inside the P&L Below net income, reconciled, adjustments named
A statement with no basis or period in its header StatementToolbar — basis and resolved period always visible
overflow-x: auto as the mobile table strategy Column priority + card list
backdrop-filter: blur() on a scrim A tinted scrim
useEffect to set mounted for an entrance @starting-style

Review process

  1. Spec review — the four layers, before any code. Reject at the earliest failing layer.
  2. Implementation review — sections 1–5 of this document.
  3. Motion & performance review — every animation at 4× slowdown, frame-stepped, on a throttled real device; section 7 measured on that same device.
  4. Financial review — sections 4 and 4b, by someone who has actually closed a month.
  5. A11y review — keyboard-only, then VoiceOver, then greyscale.
  6. Next-day review — look at it again with fresh eyes. You will find things.

Step 6 is not optional and it is not padding. It catches more real problems than any of the automated checks.


The audit pass

Everything above gates a component on the way in. This gates a surface that already shipped — a dashboard, a close flow, a payments screen — against the system that is supposed to govern it. Adapted from ibelick/ui-skills (improve-ui).

Different job, different rules, and the first one is structural: the audit never modifies product source. It reads, it proves, and it writes plans to design-plans/. Someone else executes them. An auditor who can edit starts fixing what is easy to fix rather than what is worth fixing.

The three proofs

A difference you noticed is a candidate. It becomes a finding only when all three hold:

  1. Contract — cite the binding decision. A line in foundations/, a box in this document, or a direct contradiction between two surfaces inside the same task. "Prefer", "generally", a suggestive token name, and the mere absence of a documented exception are not contracts.
  2. Runtime — prove the cited token, variant or component actually reaches this surface through the rendered path: imports, props, resolved config, CSS inheritance. Shared names, similar values and repo proximity prove nothing.
  3. Correction — state the one change the evidence requires, naming the exact existing token, variant or primitive. If the evidence supports two possible corrections, or the right answer requires inventing product intent, drop the candidate.

Then try to break it

Re-open every cited source and attempt to falsify each surviving finding. Delete it when the rule does not govern that property on that surface, when the difference turns out to be deliberate, when another finding has the same root cause, or when the correction is a preference wearing a citation.

Stop at three. An audit that returns twenty findings is a search-and-replace with a narrative attached, and nobody acts on it. Rank by evidence strength × leverage, report three, and name the one to fix first.

Scope

In scope: tokens, typography, color, spacing, layout, component variants, responsive presentation, copy, and conformance to a documented rule.

Out of scope unless explicitly requested: behavior, state, data wiring, tests, performance, architecture. Accessibility too — it has its own document and its own review step, and folding it in here buries it under spacing notes.

Zero findings beats one unsupported finding. The audit's credibility is the entire product; one invented finding costs more than ten real ones earn.

Told when this spec changes

The docs are the contract the components are built against. When one changes, the component changes with it — and the copy in your repo does not. One email when that happens.

One address, no list rental, unsubscribe in one click.

Works in your QuickBooks Online or Xero file

Get your books closed every month

Lopifi is building AI bookkeeping that keeps your books closed on US GAAP as transactions happen, with a CPA review every quarter, for $200 a month. We’re not open yet.

$200/month when we open. No card now.