The lab · 11 experiments

Small things, done properly

Each one started as a real problem on a payments product. They’re rebuilt from scratch here, with fake data, so you can poke at them.

zzz?♪♫
01

Card number field

Formats as you type without moving the caret, detects the network from the first digits and runs the Luhn check. The highlighted digits are the ones Luhn doubles.

Read the post →
Try it
Network: —Waiting for digits
02

One-time code boxes

Six boxes that behave like one field: paste anywhere and the digits spread out, backspace walks back, arrows move. The autofill hint goes on the first box only.

Try it

Type or paste a code. Try pasting 482913.

03

Money input

Groups digits in the currency’s own locale, trims to 11 whole digits and the currency’s decimals, and sends a clean number while showing a friendly one.

Read the post →
Try it

Paste “1234567.891” or type past the limits.

04

Deadline badge

One way of talking about time everywhere: days, then hours in amber, then overdue in red. Drag the slider to move the clock.

Read the post →
Try it
Respond in 7 days
  • A day or more → days, rounded down
  • Less than a day → hours, in amber
  • Past the deadline → overdue, in red, with how long ago
05

Maker-checker approval

The person who requests a debit can’t approve it. The rule lives in the UI and the API; this shows the UI half.

Try it
Signed in as

Request · REQ-0031

Debit $1,240.00 from merchant balance

06

Filter chips

Each chip owns one filter. Apply and Clear both close the chip, Escape closes everything, and focus returns to the chip.

Read the post →
Try it
PaymentFromStatusAmount
08

Copyable cells

Bank details truncate in narrow tables, but the copy button always copies the full value. If the clipboard is blocked, it selects the text instead.

Try it
Account number
GB33BUKB20201555555555
Routing (Fedwire)
026009593
Payment reference
PAY-1042-2026-10-29-LIS

Sample values only. Narrow the window: the text truncates, the copy doesn’t.

09

Read before you accept

The accept checkbox unlocks when the last line of the agreement becomes visible, using IntersectionObserver rather than scroll arithmetic.

Try it

Sample merchant agreement · v3

1. This is placeholder text standing in for clause 1. A real agreement would explain fees, settlement timelines, refunds and disputes here, in language a merchant can actually follow.

2. This is placeholder text standing in for clause 2. A real agreement would explain fees, settlement timelines, refunds and disputes here, in language a merchant can actually follow.

3. This is placeholder text standing in for clause 3. A real agreement would explain fees, settlement timelines, refunds and disputes here, in language a merchant can actually follow.

4. This is placeholder text standing in for clause 4. A real agreement would explain fees, settlement timelines, refunds and disputes here, in language a merchant can actually follow.

5. This is placeholder text standing in for clause 5. A real agreement would explain fees, settlement timelines, refunds and disputes here, in language a merchant can actually follow.

6. This is placeholder text standing in for clause 6. A real agreement would explain fees, settlement timelines, refunds and disputes here, in language a merchant can actually follow.

7. This is placeholder text standing in for clause 7. A real agreement would explain fees, settlement timelines, refunds and disputes here, in language a merchant can actually follow.

— End of agreement —

Scroll to the end to enable the checkbox.

10

Header text that picks its own colour

Merchants pick any brand colour; the header text chooses black or white from the colour’s relative luminance so it always reads.

Read the post →
Try it
₹ 4,999.00

11

A loader without a library

The kind of CSS loader that replaced an 80 KB animation library on a checkout. Two keyframes, no JavaScript.

Read the post →
Try it

0 KB of JavaScript · two keyframes · respects reduced motion

@keyframes spin { to { transform: rotate(1turn); } }
.ring {
  border: 3px solid var(--line);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin .8s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  .ring { animation: none; }
}
↑↓ move↵ openesc close