UXL · UX Lawyer

The Fernwheel case · UXL-DEMO-001 · 20 Oct 2026

One prompt, two sessions, one difference.

We asked Claude Code to build a website for Fernwheel, a fictional e-bike subscription startup. Two fresh, separate sessions got the same prompt, the same model and the same tools. The only difference: Exhibit B had UXL installed as a project skill. The prompt never mentions UX.

Exhibit P · the prompt, verbatim
We’re Fernwheel, a (fictional) e-bike subscription startup launching in Zurich, Berlin and Amsterdam. Members pay monthly (three plans from €39 to €89), and insurance, repairs and a replacement bike within 48 hours are included. Build our marketing website as a single self-contained index.html (inline CSS and JS, no build step; Google Fonts are fine, but no external images, so use CSS/SVG). It needs a homepage with our plans, a check whether we deliver to your postcode, and a form to book a free test ride. Make it stunning, and it has to work great on phones and desktop.

Exhibit A · without UXL

Time
6 min
Tool calls
23
Cost
$1.42
Skills used
none

Exhibit B · with UXL

Time
12 min
Tool calls
33
Cost
$2.91
Skills used
ux-lawyer, as its first action

The exhibits · live

Both pages, exactly as the sessions wrote them

These are the real, unedited files. Scroll inside each frame, try the postcode check (8004, 10437, 1012 AB) and book a ride.

The evidence

What the scanners measured

Both pages were scanned with UXL’s own tools after the sessions ended: uxl_lint.py on the source and uxl_audit.mjs in Chromium at 360, 390 and 1440 px.

MeasureExhibit A · withoutExhibit B · with UXL
Runtime audit score58 / 10096 / 100
Runtime findings (critical · major · minor)2 · 2 · 60 · 0 · 3
Phone tap targets under 44 px11 of 35 9 under 24 px0 of 31
Distinct font sizes rendered19–2111
Filled buttons in the first phone screen2, same action in two styles1
Booking form8 inputs at once2 steps, “Step 1 of 2”, tiles
Pending state while bookingnone, instant success (backend left as a TODO)“Booking your ride…”, double taps blocked
Unfinished booking survives a reloadnoyes
Static lint score9292
Largest paint (phone)0.2 s0.2 s
Laws cited in the source013 citations across 9 laws

Scanner scores are heuristics, and this is one run per condition. Raw data: data.json, audit A, audit B.

The counts

Where the two pages differ, law by law

At a glance both pages look launch-ready. Each count below names the law, shows the evidence from both exhibits, and states the finding.

Footer links too small for a thumb

On a phone, Exhibit A’s footer links are about 20 px tall and stacked tightly. That’s fine for a mouse and fiddly for a finger. Exhibit B gives every footer link a 44 px row. Exhibit A’s own hand-off promised “large tap targets”; the measurement says otherwise, which is why UXL measures instead of asking.

Exhibit A footer on a phone, with seven links outlined in red because they are under 44 pixels tall.
Exhibit A7 footer targets under 44 px, outlined in red.
Exhibit B footer on a phone with evenly spaced links in 44 pixel rows and a Dark mode button.
Exhibit B0 targets under 44 px.

FindingExhibit A in violation (runtime audit: critical). Exhibit B compliant.

One action, one loud button

Exhibit A shows the same action, booking a test ride, as a dark header button and a lime hero button. Next to them sits a dark, icon-only menu button: three heavy shapes compete in the first screen. Exhibit B reserves its single orange style for booking, keeps the secondary action outlined, and labels its menu “Menu”.

Exhibit A first phone screen: headline Ride more. Own less., a dark Book a test ride button, a dark round menu button and a lime Book a free test ride button.
Exhibit ATwo filled styles for one action, plus an unlabelled menu.
Exhibit B first phone screen: headline Your own e-bike, without the worries., one orange Book a free test ride button, an outlined See plans button and a labelled Menu button.
Exhibit BOne primary action, a quiet secondary, a labelled menu.

FindingExhibit A dilutes its call to action. Exhibit B compliant.

Booking: eight inputs at once, or two short steps

Exhibit A asks for name, email, phone, city, date, bike, time and consent in one form. Exhibit B splits booking into “Step 1 of 2” with a progress bar. City, bike and slot are large tiles with prices; contact details come second, next to a summary of the ride.

Exhibit A booking form on a phone: full name, email, phone, city, preferred date, bike, time of day and a consent checkbox, then a lime Book my test ride button.
Exhibit AOne long form. Scroll inside the image.
Exhibit B booking step 1 of 2 on a phone: progress bar, city chips, three bike cards with prices, day tiles, start time tiles and an orange Continue button.
Exhibit BStep 1 of 2, with choices as tappable tiles.

FindingExhibit A is acceptable but heavy. Exhibit B shows where the user is and what is left.

What happens after the tap

Exhibit A jumps straight to its success message and leaves the real request as a comment. Once a backend is connected, nothing tells the user that anything is happening. Exhibit B already shows a pending state, blocks double taps, and keeps an unfinished booking so a returning visitor can pick up where they left off.

// TODO: send the booking to your backend, e.g. fetch('/api/test-rides', {method:'POST', body:new FormData(form)})
form.hidden = true;
success.classList.add('show');
Exhibit ASubmit handler, verbatim.
btn.setAttribute('aria-busy', 'true');
btn.innerHTML = '<span class="spinner" aria-hidden="true"></span><span class="btn-label">Booking your ride…</span>';

.btn[aria-busy="true"] { pointer-events: none; }   /* CSS */

store.set('fw-draft', d);   // saveDraft()
Exhibit BSubmit handler, CSS and draft saving, verbatim excerpts.

FindingExhibit A has no feedback path. Exhibit B compliant.

A type scale, or nineteen sizes

Exhibit A renders 19 to 21 different font sizes, including 12.5, 13.5, 14.5 and 15.5 px. Exhibit B uses 11, almost all from one scale. Fewer, deliberate sizes make the hierarchy easier to read.

FindingMinor for both; noticeably tighter in Exhibit B.

For the record

What both got right, and what UXL still flags

Both exhibits

  • Visible labels and inline, specific errors on every form (§L29).
  • A forgiving postcode check: 8004, 10437 and 1012 AB all work (§L24 Postel).
  • Three plans with one honest recommendation (§L02).
  • Reduced motion respected, fast first paint, no layout shift (§L06).
  • No fake urgency or pre-ticked extras (§L04).

Still flagged in Exhibit B

  • The linter counts 10 orange buttons across the long page. The session kept them on purpose: one per section, all leading to booking.
  • A 1.4 s bike roll-in on load (UXL’s limit for UI motion is 300 ms; this is decorative and once).
  • 11 font sizes is still above the 6–8 UXL recommends.
  • The chat hand-off explained each choice but didn’t cite laws by §. The citations live in the code comments instead.

Verdict

Exhibit B.

Both sessions produced a page that looks launch-ready. Exhibit B is the one that also holds up in a thumb’s reach: every target is tappable, one action leads, booking comes in two short steps, and taps get feedback. It took about twice the time and tokens, spent on checks that caught three real mobile bugs before hand-off.

Reproduce it

Run the case yourself

Same prompt, two empty folders, one with UXL installed. Then let UXL’s scanners compare the results.

# Exhibit A: no skill
mkdir a && (cd a && claude -p "$(cat PROMPT.txt)")

# Exhibit B: same prompt, UXL installed as a project skill
git clone https://github.com/BurgiSimon/UXL
mkdir b && UXL/install.sh --project b && (cd b && claude -p "$(cat PROMPT.txt)")

# The evidence
python3 UXL/tools/build_demo.py --without a --with b --prompt PROMPT.txt