Nº 581 of 605
Card controls with frost freeze
iOS card settings: a drawn copper debit card that ices over when frozen, a monthly limit slider with a spent marker, where-it-works switches and declines.
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 "Card controls with frost freeze" (piece fintech-card-controls) 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º 581 · "Card controls with frost freeze" · www.designlounge.live -->
# Card controls with frost freeze
> **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 card settings screen of an invented bank, Tidewell, in an iOS 26 language: a translucent sticky nav bar, inset grouped lists on a warm stone ground, 51×31 switches, SF-like type set in Instrument Sans. At the top sits the physical card drawn in CSS: copper gradient, engraved wave lines, a chip and contactless mark. Freezing the card does not just flip a switch; frost grows across the card from its top-right corner, six-armed crystals settle in with a stagger, and the card's print turns from cream to a deep ice-ink so it reads through the frost. Below: a monthly spending limit slider with a marker for what has already been spent, three where-it-works switches (online, ATM, abroad) and a short list of this week's declined payments with a one-tap fix.
## Reference behaviour
1. Initial state: the card is **frozen** (the hero look). Frost covers the card, four crystals are visible, the status line reads "**Frozen** since 2:41 pm. Nothing new can be charged." with a blue dot, the first row reads "Card frozen / Subscriptions and refunds still go through" and its switch is on.
2. Tap the switch, or tap the card itself: the frost retracts into the top-right corner as a shrinking circle (420ms), crystals fade and scale to 0.4, print colour returns to cream, the status line becomes "**Active.** Last used today, Corva Market, $18.40" with a green dot, and the row reads "Freeze card / Stops new purchases. Undo any time".
3. Tap again: frost grows as `circle(0% → 150% at 92% 8%)` over 720ms expo-out; crystals arrive 120ms later, staggered 0 / 90 / 160 / 220ms, each from `scale(.4) rotate(-20deg)` to rest.
4. Drag the spending-limit slider (0 to $5,000, step $100, starts at $2,400): the 28px output updates live; the sub-line reads "$1,186.40 spent since Oct 1 · $1,213.60 left"; drop below $1,186.40 and it reads "$1,186.40 spent. New purchases decline until Nov 1". At 0 the output reads "No spending". A 2×20px ink tick labelled "Spent" marks $1,186.40 on the track.
5. The three where-it-works switches toggle independently. "Payments abroad" starts off; its sub-line reads "Off. Turn on before you travel".
6. In "Declined this week", Halden Air ($412.00, Reykjavik) shows a rust tag "Payments abroad are off" and a filled button "Allow abroad for 24 hours". Tapping it turns the abroad switch on, the sub-line becomes "On until Oct 29, 2:41 pm" and the button disables to "Allowed until tomorrow, 2:41 pm". Turning the switch off by hand restores the button.
7. The second decline (Pellmore Games, $59.99, "Card was frozen") is informational only.
## Structure
```
390 × 844 (54px status area drawn by the Lounge)
┌──────────────────────────────────────┐
│ 54 safe area (bar padding) │
│ ‹ Cards Card controls ··· │ 44 sticky glass bar
├──────────────────────────────────────┤
│ ┌──────────────────────────────────┐ │ 16 side gutter
│ │ Tidewell DEBIT │ │ card 358 × 226 (1.586:1), r16
│ │ [chip] ))) │ │
│ │ ···· 2290 Valid thru│ │
│ │ NOOR HALVORSEN 08/29 │ │
│ └──────────────────────────────────┘ │
│ ● Frozen since 2:41 pm … │ status, live
│ ┌ [*] Card frozen (● ) ┐ │ group r14
│ SPENDING LIMIT │
│ ┌ Per month $2,400 ┐ │
│ │ $1,186.40 spent … $1,213.60 left │ │
│ │ ━━━━━━━━|━━━━━○──────────────── │ │ 6px track, 28px thumb
… 194 more lines. Copy or download for the full brief. Loading the source…Brief
234 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 card settings screen of an invented bank, Tidewell, in an iOS 26 language: a translucent sticky nav bar, inset grouped lists on a warm stone ground, 51×31 switches, SF-like type set in Instrument Sans. At the top sits the physical card drawn in CSS: copper gradient, engraved wave lines, a chip and contactless mark. Freezing the card does not just flip a switch; frost grows across the card from its top-right corner, six-armed crystals settle in with a stagger, and the card’s print turns from cream to a deep ice-ink so it reads through the frost. Below: a monthly spending limit slider with a marker for what has already been spent, three where-it-works switches (online, ATM, abroad) and a short list of this week’s declined payments with a one-tap fix.
Reference behaviour
- Initial state: the card is frozen (the hero look). Frost covers the card, four crystals are visible, the status line reads “Frozen since 2:41 pm. Nothing new can be charged.” with a blue dot, the first row reads “Card frozen / Subscriptions and refunds still go through” and its switch is on.
- Tap the switch, or tap the card itself: the frost retracts into the top-right corner as a shrinking circle (420ms), crystals fade and scale to 0.4, print colour returns to cream, the status line becomes “Active. Last used today, Corva Market, $18.40” with a green dot, and the row reads “Freeze card / Stops new purchases. Undo any time”.
- Tap again: frost grows as
circle(0% → 150% at 92% 8%)over 720ms expo-out; crystals arrive 120ms later, staggered 0 / 90 / 160 / 220ms, each fromscale(.4) rotate(-20deg)to rest. - Drag the spending-limit slider (0 to $5,000, step $100, starts at $2,400): the 28px output updates live; the sub-line reads “$1,186.40 spent since Oct 1 · $1,213.60 left”; drop below $1,186.40 and it reads “$1,186.40 spent. New purchases decline until Nov 1”. At 0 the output reads “No spending”. A 2×20px ink tick labelled “Spent” marks $1,186.40 on the track.
- The three where-it-works switches toggle independently. “Payments abroad” starts off; its sub-line reads “Off. Turn on before you travel”.
- In “Declined this week”, Halden Air ($412.00, Reykjavik) shows a rust tag “Payments abroad are off” and a filled button “Allow abroad for 24 hours”. Tapping it turns the abroad switch on, the sub-line becomes “On until Oct 29, 2:41 pm” and the button disables to “Allowed until tomorrow, 2:41 pm”. Turning the switch off by hand restores the button.
- The second decline (Pellmore Games, $59.99, “Card was frozen”) is informational only.
Structure
390 × 844 (54px status area drawn by the Lounge)
┌──────────────────────────────────────┐
│ 54 safe area (bar padding) │
│ ‹ Cards Card controls ··· │ 44 sticky glass bar
├──────────────────────────────────────┤
│ ┌──────────────────────────────────┐ │ 16 side gutter
│ │ Tidewell DEBIT │ │ card 358 × 226 (1.586:1), r16
│ │ [chip] ))) │ │
│ │ ···· 2290 Valid thru│ │
│ │ NOOR HALVORSEN 08/29 │ │
│ └──────────────────────────────────┘ │
│ ● Frozen since 2:41 pm … │ status, live
│ ┌ [*] Card frozen (● ) ┐ │ group r14
│ SPENDING LIMIT │
│ ┌ Per month $2,400 ┐ │
│ │ $1,186.40 spent … $1,213.60 left │ │
│ │ ━━━━━━━━|━━━━━○──────────────── │ │ 6px track, 28px thumb
│ │ $0 Spent $5,000│ │
│ WHERE IT WORKS │
│ ┌ Online and in apps (● ) ┐ │ 60px cells
│ │ ATM withdrawals (● ) │ │
│ └ Payments abroad ( ○) ┘ │
│ DECLINED THIS WEEK │
│ ┌ (HA) Halden Air $412.00 ┐ │
│ │ [Payments abroad are off] │ │
│ │ [Allow abroad for 24 hours] │ │
│ └ (PG) Pellmore Games $59.99 ┘ │
│ footnote │
└──────────────────────────────────────┘
<header class="bar">sticky,padding-top: 54px,backdrop-filter: blur(18px) saturate(1.6)overrgba(239,237,232,.78); a 3-column grid with a back<button>,<h1>, and a 44px more button..card(role="img",aria-labelwith the last four digits): engraving SVG (z 0),.frost(z 1, holds the crystal SVGs), then the print rows, chip and contactless mark (z 2).- Each settings row is
.cell: 32px tinted icon tile,.txt(label + sub-line),<button role="switch" aria-checked>. Separators are inset 62px from the left, iOS style. - Limit block: label row with
<output>, sub-line,.rngwrapper containing<input type="range">, the spent tick and its label. - Declines:
.decrows with a 36px initials disc, name/amount row, meta line, reason tag, optional action<button>.
Tokens
:root {
/* ground */
--bg: #EFEDE8; /* warm stone, grouped background */
--cell: #FDFCFA; /* group surface */
--sep: #E2DED6; /* inset separators */
--ink: #1D1B18;
--ink-2: #5B5650; /* sub-lines (6.6:1 on cell) */
--ink-3: #6E6962; /* section headers, scale (5.0:1 on bg) */
/* the accent: deep ice blue, used for switches, slider fill, links, the one action */
--accent: #1F5F8B;
--accent-wash: #E1ECF4; /* icon tiles, disabled action */
--off: #8A867F; /* off switch track and empty slider track (3.6:1 on cell) */
/* status */
--live: #2E7D4F; /* active dot */
--warn: #9A3B1E; /* decline tag text */
--warn-wash: #F6E6DE; /* decline tag fill */
/* the card */
--copper-1: #A84B2A;
--copper-2: #7A3019;
--copper-glow: #AE5230;
--print: #FFF7F2; /* card text, unfrozen */
--frost-ink: #12324A; /* card text, frozen */
--frost-1: rgba(230, 240, 247, .86);
--frost-2: rgba(200, 220, 234, .80);
/* type */
--font: "Instrument Sans", system-ui, sans-serif;
/* shape */
--gutter: 16px;
--r-group: 14px;
--r-card: 16px;
--r-btn: 12px;
--cell-h: 60px;
--switch-w: 51px; --switch-h: 31px; --knob: 27px;
--shadow-card: 0 1px 0 rgba(255,255,255,.25) inset, 0 18px 30px -18px rgba(90,30,10,.6), 0 2px 6px rgba(29,27,24,.12);
/* motion */
--t-fast: 180ms;
--t-knob: 260ms;
--t-freeze: 720ms;
--t-thaw: 420ms;
--spring: cubic-bezier(.32, .72, 0, 1);
--ease: cubic-bezier(.2, .7, .2, 1);
--ease-out: cubic-bezier(.16, 1, .3, 1);
}
Typography
All Instrument Sans, font-variant-numeric: tabular-nums on the body.
| Role | Size | Weight | Line-height | Tracking | Case |
|---|---|---|---|---|---|
| Nav title | 17px | 600 | 1.35 | 0 | Title |
| Back / bar links | 17px | 400 | 1.35 | 0 | Title |
| Row label | 16px | 600 | 1.35 | 0 | sentence |
| Row sub-line | 13px | 400 | 1.35 | 0 | sentence |
| Section header | 13px | 600 | 1.35 | +0.04em | UPPERCASE |
| Limit output | 28px | 600 | 1.1 | −0.02em | numerals |
| Card wordmark | 17px | 700 | 1 | −0.01em | Title |
| Card number | 19px | 500 | 1 | +0.12em | numerals |
| Card name, kind | 12px | 600 | 1 | +0.10em / +0.14em | UPPERCASE |
| Decline tag | 12px | 600 | 1.35 | 0 | sentence |
| Action button | 15px | 600 | 40px box | 0 | sentence |
Motion
| Element | Trigger | Property | From → To | Duration | Easing / delay |
|---|---|---|---|---|---|
.frost | freeze | clip-path | circle(0% at 92% 8%) → circle(150% …) | 720ms | --ease-out |
.frost | unfreeze | clip-path | 150% → 0% | 420ms | --ease |
| Crystals | freeze | opacity, transform | 0, scale(.4) rotate(-20deg) → .95, none | 500 / 600ms | --ease-out, 120ms + 0/90/160/220ms |
| Crystals | unfreeze | same, reversed | 420ms | --ease, no stagger | |
| Card print colour | freeze toggle | color | --print ↔ --frost-ink | 720ms | --ease-out |
| Chip | freeze | filter | none → saturate(.25) brightness(1.15) | instant with frost | — |
| Card | press | transform | 1 → .985 | 180ms | --spring |
| Switch knob | toggle | translateX | 0 → 20px | 260ms | --spring |
| Switch knob | press | width | 27 → 32px (stretch) | 0 | — |
| Switch track | toggle | background | --off → --accent | 180ms | --ease |
| Action button | press | transform | 1 → .96 | 180ms | --spring |
Reduced motion: all transitions and delays drop to 1ms. Frost appears and disappears at once; nothing else changes.
States
- Frozen:
body.frozen(or a frozen flag on the card component). Frost visible, print--frost-ink, chip desaturated, status dot--accent, row label “Card frozen”. - Active: frost clipped to 0%, print
--print, status dot--live. - Switch on/off:
aria-checked="true"fills--accentand moves the knob 20px; off is--off, never a pale grey (it must hold 3:1 on the white cell). - Slider: fill
--accentup to the value (background-size: var(--p) 100%over a--offbackground-color); 28px white thumb with a 3px/8px shadow. - Over limit: sub-line swaps to the decline sentence when the limit is under the amount spent.
- Decline fixed: action button
disabled, fill--accent-wash, text--accent, label “Allowed until tomorrow, 2:41 pm”. - Focus-visible: 2px
--accentoutline, 2px offset, on every button, switch and the slider.
Accessibility
- Switches are
<button role="switch" aria-checked>labelled by their row label viaaria-labelledby. Space and Enter toggle (native button behaviour). - The card is
role="img"with “Tidewell everyday debit card ending 2290”; tapping it is a shortcut, the switch is the real control. - The status line is
aria-live="polite"so freezing and unfreezing are announced. - The slider keeps native arrow-key stepping ($100) and sets
aria-valuetextto “$2,400 per month”. - Hit targets: switches 51×31 inside a 60px row, bar buttons 44×44, action button 40px tall with full-width text.
- Contrast: frost-ink on the frosted copper is about 7:1; card print on copper 4.8:1 at the lightest corner (the glow is capped at #AE5230 for that reason);
--offagainst--cell3.6:1 for the control edge.
Responsive rules
- 390: as specified; the screen scrolls, the bar stays sticky and blurs content under it.
- 360: the card keeps 1.586:1 at 328px wide; card number drops to 17px; row sub-lines wrap to two lines.
- Tablet: centre a 440px column on
--bg; the card stays at that column’s width. - Never scroll sideways; amounts in decline rows stay on the name line, right aligned.
Acceptance checklist
Always
- The card is drawn, not an image, at a 1.586:1 ratio with a 16px radius.
- Freezing grows frost from the top-right corner over 720ms; unfreezing retracts it over 420ms.
- Card text changes to a dark ice ink while frozen and keeps 4.5:1 on the frost.
- Tapping the card toggles freeze exactly like the switch.
- Switches are 51×31 with
role="switch", and the off track holds 3:1 against the cell. - The limit slider shows the amount already spent as a tick on the track.
- The status line is a polite live region.
- One filled action button on the screen.
- Reduced motion removes every transition but keeps both states complete.
This demo
- Bank “Tidewell”, card ending 2290, NOOR HALVORSEN, 08/29.
- Limit starts at $2,400 of $5,000 with $1,186.40 spent and $1,213.60 left.
- Online and ATM on, Payments abroad off.
- Halden Air $412.00 decline offers “Allow abroad for 24 hours”, which sets “On until Oct 29, 2:41 pm”.
Implementation notes
Frost as a clipped layer between engraving and print. Order the card’s children by z-index so print always sits above the ice, and grow the ice with clip-path from the corner:
.card .eng { z-index: 0; }
.frost {
position: absolute; inset: 0; z-index: 1; pointer-events: none;
background: linear-gradient(115deg, transparent 30%, rgba(255,255,255,.35) 46%, transparent 60%),
linear-gradient(160deg, var(--frost-1), var(--frost-2));
box-shadow: inset 0 0 24px 6px rgba(255,255,255,.55);
clip-path: circle(0% at 92% 8%);
transition: clip-path var(--t-thaw) var(--ease);
}
.frozen .frost { clip-path: circle(150% at 92% 8%); transition: clip-path var(--t-freeze) var(--ease-out); }
.frozen .card { color: var(--frost-ink); }
Staggered crystals with a per-element variable. Each crystal SVG carries --d; only the freezing direction uses it, so thawing is a single quick fade:
.frost svg { opacity: 0; transform: scale(.4) rotate(-20deg); transition: all var(--t-thaw) var(--ease); }
.frozen .frost svg { opacity: .95; transform: none;
transition: opacity 500ms var(--ease-out) calc(var(--d) + 120ms), transform 600ms var(--ease-out) calc(var(--d) + 120ms); }
Put the spent tick under the thumb’s travel, not the track’s width. The thumb centre moves from 14px to width − 14px, so offset the tick by the same rule:
const f = SPENT / 5000;
rng.style.setProperty('--s', `calc(${f * 100}% - ${f * 28 - 14}px)`);
Common mistakes: a pale grey off-switch that disappears on white; covering the card print with the frost so the number becomes unreadable; animating backdrop-filter (expensive, and it blurs the print too).
Details
- Palette
- Type
- Instrument Sans
- Motion
- Rich motion
- Build
- One session
- Tags