Nº 621 of 629
Slide to pay back a friend
A gold handle you drag along an ink track to repay a friend: letters dim as it passes, it resists near the end, snaps through at 90% and becomes a receipt.
Preview
Use it
Free to use · credit appreciated1 Once, in your project · Node 22.20+
npx skills add SusanAcharya/Design-Lounge 2 Then ask
Using the Design Lounge skill, build "Slide to pay back a friend" (piece button-slide-to-confirm) in my stack. Match its brief; restyle it onto my product's theme.
No skill? Paste the brief instead. It carries the numbers, tokens and behaviour.
<!-- Design Lounge Nº 621 · "Slide to pay back a friend" · www.designlounge.live -->
# Slide to pay back a friend
> **Build brief for a coding agent.** Rebuild this piece in the reader's stack. If they haven't said which stack, ask once, then default to semantic HTML + CSS + a little vanilla JS. Match the numbers below; don't "improve" them.
## What it is
The settle-up screen of "Quits", an invented app for splitting group costs. You owe Maren £64.20 for a cabin weekend; at the bottom of the screen sits an ink pill with a gold handle. Drag the handle and the label "Slide to pay £64.20" vanishes letter by letter as the handle passes over it; past 72% the handle starts to resist; reach 90% and it snaps through, then grows to fill the whole track and turns into a receipt: check, "£64.20 sent", time. Let go short and it springs home with a small bounce. The action is irreversible but happy, so the friction is light and the payoff is warm, not alarming. Worth copying: the per-letter dimming, the rubber-band curve, and a keyboard path (arrows, then Enter twice) that is as deliberate as the drag.
## Reference behaviour
1. First frame, 390 × 844 iOS-style (Lounge draws the status bar): 62px top padding. Back button (40px circle, 1.5px border) and "Cabin weekend / Hartsfell · 3 people · 24 to 26 Oct".
2. A 44px peach avatar "MO" with "You owe Maren Okafor / She paid for the cabin and the Friday shop", then the amount "£64.20" at 88px Mona Sans, width 125%, weight 760, with a raised 0.42em "£".
3. Note: "Your share of the weekend. Payments can't be recalled, so check the amount." Then a card: "Cabin, 2 nights, your third £48.00", "Friday shop, Penhallow Stores £11.70", "Fuel, split three ways £4.50", "From Everyday ·· 3318 Arrives now".
4. Bottom of the screen (pinned by `margin-top: auto`): a 68px ink track, 2px ink border, radius 34; a 56px gold handle with an arrow at left 4px; centred cream label "Slide to pay £64.20" followed by a small gold arrow. Under it, 13px: "Drag to the end, or press Enter twice."
5. 700ms after load the handle nudges once: 14px right, 2px back, home (900ms). It never loops.
6. Pointer down on the handle captures the pointer. The handle follows the finger 1:1 until 72% of travel; after that, travel follows `0.72 + 0.28 · tanh((r − 0.72)/0.28 · 1.1)` so the last stretch feels heavy (reaching 90% needs about 91% of finger travel).
7. While dragging, a darker trail (`#3a3123`) grows behind the handle; each letter whose centre the handle's right edge has passed goes to opacity 0; letters within 56px ahead dim from 0.2 to 1; the whole label also fades by `1 − 0.45p`.
8. The moment position reaches 90%, the drag ends by itself (snap-through): the handle springs to 100% (k 900, c 46), then grows to the full track width in 440ms expo-out while its left edge slides to 0, so the right edge stays put.
9. Success: the arrow becomes a check that draws itself (360ms, 180ms delay); "£64.20 sent" and the time (e.g. "14:32", tabular) fade in 200ms later. The heading becomes "You're square with Maren", the amount greys to `#5b5549` and a 7px gold-deep line strikes through it (520ms, 120ms delay), the note becomes "Paid from Everyday ·· 3318 at 14:32. Cabin weekend is settled for you.", the hint becomes "Maren gets a note from Quits right away." and a "Replay" link appears (its space is reserved, so nothing jumps).
10. Release short of 90%: the handle springs home (k 340, c 17) and bounces once off the left wall; if it was past 25% the live region says "Not sent."
11. Keyboard: Tab to the handle (cream 3px ring inset 7px). Right/Up +10%, Left/Down −10% (capped at 80%, arrows alone never send). Enter or Space arms: the handle glides to 84%, "Press Enter again to send £64.20" fades in above the track, and the live region says "Press Enter again to send £64.20 to Maren. Escape cancels." A second Enter within 5s sends. Escape or Home, or 5s without a second Enter, springs it home and says "Not sent."
12. On success a polite live region announces "Paid Maren £64.20 at 14:32. You're square."
13. Reduced motion: no nudge, no springs, no morph transition; the handle jumps home or straight to the receipt.
## Structure
```
390 × 844 (column max-width 420, centred on wider screens)
┌──────────────────────────────────────┐ 62px top pad
│ (‹) Cabin weekend │ back 40×40
│ Hartsfell · 3 people · 24… │
│ │ 34
│ (MO) You owe Maren Okafor │ avatar 44
│ She paid for the cabin… │
│ £64.20 88px / 125% │
│ Your share of the weekend… │ note 15px, max 330
│ ┌──────────────────────────────────┐ │ card r18, 1px #ddd5c4
│ │ Cabin, 2 nights, your third £48.00│ │ rows 11px v-pad
… 187 more lines. Copy or download for the full brief. Loading the source…Brief
227 linesBuild brief for a coding agent. Rebuild this piece in the reader’s stack. If they haven’t said which stack, ask once, then default to semantic HTML + CSS + a little vanilla JS. Match the numbers below; don’t “improve” them.
What it is
The settle-up screen of “Quits”, an invented app for splitting group costs. You owe Maren £64.20 for a cabin weekend; at the bottom of the screen sits an ink pill with a gold handle. Drag the handle and the label “Slide to pay £64.20” vanishes letter by letter as the handle passes over it; past 72% the handle starts to resist; reach 90% and it snaps through, then grows to fill the whole track and turns into a receipt: check, “£64.20 sent”, time. Let go short and it springs home with a small bounce. The action is irreversible but happy, so the friction is light and the payoff is warm, not alarming. Worth copying: the per-letter dimming, the rubber-band curve, and a keyboard path (arrows, then Enter twice) that is as deliberate as the drag.
Reference behaviour
- First frame, 390 × 844 iOS-style (Lounge draws the status bar): 62px top padding. Back button (40px circle, 1.5px border) and “Cabin weekend / Hartsfell · 3 people · 24 to 26 Oct”.
- A 44px peach avatar “MO” with “You owe Maren Okafor / She paid for the cabin and the Friday shop”, then the amount “£64.20” at 88px Mona Sans, width 125%, weight 760, with a raised 0.42em ”£”.
- Note: “Your share of the weekend. Payments can’t be recalled, so check the amount.” Then a card: “Cabin, 2 nights, your third £48.00”, “Friday shop, Penhallow Stores £11.70”, “Fuel, split three ways £4.50”, “From Everyday ·· 3318 Arrives now”.
- Bottom of the screen (pinned by
margin-top: auto): a 68px ink track, 2px ink border, radius 34; a 56px gold handle with an arrow at left 4px; centred cream label “Slide to pay £64.20” followed by a small gold arrow. Under it, 13px: “Drag to the end, or press Enter twice.” - 700ms after load the handle nudges once: 14px right, 2px back, home (900ms). It never loops.
- Pointer down on the handle captures the pointer. The handle follows the finger 1:1 until 72% of travel; after that, travel follows
0.72 + 0.28 · tanh((r − 0.72)/0.28 · 1.1)so the last stretch feels heavy (reaching 90% needs about 91% of finger travel). - While dragging, a darker trail (
#3a3123) grows behind the handle; each letter whose centre the handle’s right edge has passed goes to opacity 0; letters within 56px ahead dim from 0.2 to 1; the whole label also fades by1 − 0.45p. - The moment position reaches 90%, the drag ends by itself (snap-through): the handle springs to 100% (k 900, c 46), then grows to the full track width in 440ms expo-out while its left edge slides to 0, so the right edge stays put.
- Success: the arrow becomes a check that draws itself (360ms, 180ms delay); “£64.20 sent” and the time (e.g. “14:32”, tabular) fade in 200ms later. The heading becomes “You’re square with Maren”, the amount greys to
#5b5549and a 7px gold-deep line strikes through it (520ms, 120ms delay), the note becomes “Paid from Everyday ·· 3318 at 14:32. Cabin weekend is settled for you.”, the hint becomes “Maren gets a note from Quits right away.” and a “Replay” link appears (its space is reserved, so nothing jumps). - Release short of 90%: the handle springs home (k 340, c 17) and bounces once off the left wall; if it was past 25% the live region says “Not sent.”
- Keyboard: Tab to the handle (cream 3px ring inset 7px). Right/Up +10%, Left/Down −10% (capped at 80%, arrows alone never send). Enter or Space arms: the handle glides to 84%, “Press Enter again to send £64.20” fades in above the track, and the live region says “Press Enter again to send £64.20 to Maren. Escape cancels.” A second Enter within 5s sends. Escape or Home, or 5s without a second Enter, springs it home and says “Not sent.”
- On success a polite live region announces “Paid Maren £64.20 at 14:32. You’re square.”
- Reduced motion: no nudge, no springs, no morph transition; the handle jumps home or straight to the receipt.
Structure
390 × 844 (column max-width 420, centred on wider screens)
┌──────────────────────────────────────┐ 62px top pad
│ (‹) Cabin weekend │ back 40×40
│ Hartsfell · 3 people · 24… │
│ │ 34
│ (MO) You owe Maren Okafor │ avatar 44
│ She paid for the cabin… │
│ £64.20 88px / 125% │
│ Your share of the weekend… │ note 15px, max 330
│ ┌──────────────────────────────────┐ │ card r18, 1px #ddd5c4
│ │ Cabin, 2 nights, your third £48.00│ │ rows 11px v-pad
│ │ Friday shop, Penhallow… £11.70│ │
│ │ Fuel, split three ways £4.50│ │
│ │ From Everyday ·· 3318 Arrives now│ │
│ └──────────────────────────────────┘ │
│ (flex space) │
│ Press Enter again to send £64.20 │ arm line, opacity 0 until armed
│ ┌──────────────────────────────────┐ │
│ │(→) Slide to pay £64.20 → │ │ track 68, handle 56 @ left 4 top 4
│ └──────────────────────────────────┘ │
│ Drag to the end, or press Enter… │ 13px
│ Replay │ 40px, hidden (space kept)
└──────────────────────────────────────┘ 34px bottom pad
.appis a flex column withmin-height: 100%; the footer block takesmargin-top: auto.- Header is
headerwith a realbutton(aria-label “Back to Cabin weekend”). - Amount is a
pwitharia-label="64 pounds 20"; the bill is adlofdiv > dt + ddrows. - Track is a
div; inside it a trail span, anaria-hiddenlabel span whose text is split into oneiper character, and the handlespan role="slider" tabindex="0"holding anaria-hiddenicon box and anaria-hiddenreceipt box. - Handle ARIA:
aria-label="Slide to pay Maren £64.20",aria-describedbythe hint,aria-valuemin 0,aria-valuemax 100,aria-valuenow,aria-valuetext(“Not sent”, “40 percent”, “Ready. Press Enter to send”, “Sent”). - One visually hidden
p aria-live="polite".
Tokens
:root {
/* colour */
--paper: #f3eee4; /* screen */
--card: #fbf8f1; /* bill card */
--ink: #1c1a15; /* text, track */
--ink-2: #5b5549; /* secondary text, paid amount, 6.4:1 */
--line: #ddd5c4; /* hairlines, card border */
--back-edge: #8a8272; /* back button border */
--gold: #f0b23a; /* handle, label arrow */
--gold-deep: #c98a12; /* handle top bevel, strike line */
--trail: #3a3123; /* drag trail on the ink track */
--cream: #f7f1e3; /* label on ink, handle focus ring */
--peach: #f6d9c4; /* avatar */
/* type */
--display: "Mona Sans", system-ui, sans-serif; /* variable wdth 75–125 */
--sans: "Atkinson Hyperlegible Next", system-ui, sans-serif;
--fs-amount: 88px; --fs-label: 16px; --fs-body: 15px; --fs-small: 14px; --fs-hint: 13px;
/* geometry */
--track-h: 68px; --track-r: 34px; --track-border: 2px;
--handle: 56px; --handle-inset: 4px; /* travel = track.clientWidth − 64 */
--label-left: 72px; --label-right: 18px;
--r-card: 18px; --avatar: 44px; --back: 40px;
--pad-top: 62px; --pad-x: 16px; --pad-bottom: 34px;
--space: 4px 8px 10px 12px 16px 18px 24px 34px;
/* behaviour */
--resist-from: .72; /* tanh rubber band starts here */
--snap-at: .90; /* snap-through threshold */
--arm-at: .84; /* where Enter parks the handle */
--key-step: .10; --key-max: .80; --arm-timeout: 5000ms;
/* motion */
--out: cubic-bezier(.16,1,.3,1);
--ease: cubic-bezier(.2,.7,.2,1);
--morph: 440ms; --check-draw: 360ms; --strike: 520ms; --nudge: 900ms;
--spring-home: 340 17; --spring-snap: 900 46; --spring-key: 600 40; /* stiffness, damping */
}
Typography
| Role | Family | Size | Weight | Width | Tracking | Notes |
|---|---|---|---|---|---|---|
| Amount | Mona Sans | 88px (68px if height < 700) | 760 | 125% | −0.035em | tabular, ”£” at 0.42em raised |
| Track label | Mona Sans | 16px | 650 | 112% | 0.01em | cream on ink, 15.4:1 |
| Receipt amount | Mona Sans | 16px | 700 | 110% | 0 | ink on gold, 9.2:1 |
| Avatar initials | Mona Sans | 15px | 700 | 110% | 0 | |
| Header title | Atkinson Hyperlegible Next | 15px | 600 | — | 0 | |
| Body, note | Atkinson Hyperlegible Next | 15–16px | 400 | — | 0 | line-height 1.45 |
| Bill rows | Atkinson Hyperlegible Next | 14px | 400 / 600 values | — | 0 | values tabular |
| Hint, sublines, time | Atkinson Hyperlegible Next | 13px | 400 / 600 | — | 0 |
Motion
| Thing | Trigger | Property | From → to | Duration / spring | Reduced motion |
|---|---|---|---|---|---|
| Nudge | load, once | translateX | 0 → 14 → −2 → 0 | 900ms —out, 700ms delay | none |
| Drag | pointer move | —x (translateX) | follows finger, tanh past 72% | 0 | same |
| Letter dim | —x changes | opacity per letter | 1 → 0.2 → 0 | 90ms linear | same |
| Spring home | release < 90%, Escape, timeout | —x | x → 0, wall bounce v × −0.3 | k 340, c 17 | instant |
| Key step | arrows | —x | ±10% | k 600, c 40 | instant |
| Arm | first Enter | —x; arm line opacity, translateY | → 84%; 0/4px → 1/0 | k 600, c 34; 180ms —ease | instant |
| Snap-through | x ≥ 90% | —x | → 100% | k 900, c 46 | instant |
| Morph | after snap | handle width, translateX | 56 → 100% − 8px; max → 0 | 440ms —out | instant |
| Check | done | stroke-dashoffset | 24 → 0 | 360ms —out, 180ms delay | instant |
| Receipt text | done | opacity, translateX | 0/−10px → 1/0 | 240/320ms, 200ms delay | instant |
| Strike | done | scaleX on amount ::after | 0 → 1 (origin left) | 520ms —out, 120ms delay | instant |
States
- Rest: handle gold with a 2px
#c98a12inset top bevel; label full cream. - Dragging: cursor grabbing, trail visible, letters dimming, label fading.
- Resisting: past 72% the handle moves less than the finger; no extra visual, the lag is the signal.
- Armed (keyboard): handle at 84%, “Press Enter again to send £64.20” visible above,
aria-valuetext“Ready. Press Enter to send”. - Done: track filled by the gold pill, check + “£64.20 sent” + time; amount struck; Replay visible.
- Focus-visible: 3px cream outline, offset −7px, radius 28 (sits inside the gold so it reads on ink and on gold).
- Disabled (if your product needs it): track
#ddd5c4, handle#ece6d8, no trail,aria-disabled="true".
Accessibility
- The handle is
role="slider"so screen readers announce progress; its name says exactly what sending does, andaria-describedbypoints at the visible hint “Drag to the end, or press Enter twice.” - Arrows alone cannot send (capped at 80%). Sending by keyboard always takes two Enters within 5s, the keyboard version of dragging to the end. Escape and Home cancel.
- The visible label is
aria-hidden(the handle has the name); the receipt isaria-hiddenand announced through the live region instead. - Polite live messages: arm prompt, “Not sent.”, the success sentence, and “Reset. £64.20 to Maren, not sent.” after Replay (focus returns to the handle).
touch-action: noneanduser-select: noneon the track and handle so a horizontal drag never scrolls or selects.- Contrast: ink track against paper 15.0:1 (the track edge), cream label on ink 15.4:1, ink on gold 9.2:1, secondary text 6.4:1. Letters only dim while the user is dragging, never at rest.
- Hit targets: handle 56 × 56; back button 40 × 40; Replay 40px tall.
Responsive rules
- 360 wide: track 324px, travel 264px; the label still fits on one line (white-space: pre).
- Heights under 700px: amount 68px, smaller top gaps; the track stays pinned to the bottom.
- Tablet or desktop: the column caps at 420px and centres; nothing stretches.
- Travel and letter centres are re-measured on pointer down, on each key, after fonts load and on resize; a done track stays full width.
Acceptance checklist
Always
- The handle follows the pointer with pointer capture, 1:1 up to 72%, rubber-banded after.
- Letters the handle has passed are hidden; the next 56px of letters dim progressively.
- Crossing 90% completes immediately without waiting for release; release short springs home with one bounce.
- Completion morphs the handle into a full-width receipt with check, amount and time, keeping its right edge fixed.
- Keyboard: arrows move ±10% up to 80%; Enter arms; a second Enter within 5s sends; Escape/Home/timeout cancel.
-
role="slider"with value now and value text kept current; a polite live region announces arm, cancel and success. - Track edge ≥ 3:1 against the screen; focus ring visible on gold and on ink.
- Reduced motion removes the nudge, springs and morph but keeps every state.
- No layout shift when the Replay link appears.
This demo
- Copy “You owe Maren Okafor”, “£64.20”, “Slide to pay £64.20”, “£64.20 sent”, “You’re square with Maren”.
- Bill rows £48.00, £11.70, £4.50 and “From Everyday ·· 3318”.
- Track
#1c1a1568px, handle#f0b23a56px, trail#3a3123, strike#c98a12. - Mona Sans at width 125% for the amount; Atkinson Hyperlegible Next for body.
Implementation notes
Rubber band and snap-through. Map raw finger progress to handle progress, and complete inside pointermove:
const resist = r => r <= .72 ? r : .72 + .28 * Math.tanh((r - .72) / .28 * 1.1);
handle.addEventListener('pointermove', e => {
if (!drag) return;
x = resist(Math.max(0, ox + e.clientX - sx) / max) * max; draw();
if (x >= max * .9) { drag = false; complete(); } // snaps through, no release needed
});
handle.addEventListener('pointerup', () => { if (drag) { drag = false; springTo(0, 340, 17); } });
Letters that dim as the handle passes. Split the label once, cache each letter’s centre relative to the track, then compare with the handle’s right edge every frame:
const edge = 4 + x + 56; // inset + travel + handle width
letters.forEach((l, i) => {
const d = cx[i] - edge;
l.style.opacity = (d < 0 ? 0 : d < 56 ? .2 + .8 * d / 56 : 1) * (1 - p * .45);
});
Morph without measuring. Animate width and translate together with the same curve; because x + 56 = max + 56 = width, the right edge never moves:
.track.done .handle {
width: calc(100% - 8px); transform: translateX(0);
transition: width 440ms var(--out), transform 440ms var(--out);
}
Common mistakes:
- Completing on release only; the snap-through is what makes 90% feel like a detent.
- Letting arrow keys reach 100% and send; keep a separate, two-step Enter confirmation.
- Measuring letter positions before the web font loads; re-measure on
document.fonts.ready. - Dimming the label at rest or using a shimmer that drops contrast below 4.5:1.
- Using
display: nonefor the Replay link, which shifts the track up the moment payment lands.
Details
- Palette
- Type
- Mona Sans · Atkinson Hyperlegible Next
- Motion
- Rich motion
- Build
- One session
- Tags
Studied
WickretLet the big words move. Letters can break apart and come back as new words.
All sources