Design Lounge
Install

Nº 254 of 520

Glass credit card with flip

A frosted-glass payment card that tilts to the pointer with a moving holographic edge, flips to a back with a hold-to-reveal CVV, and switches between three cards.

Preview

Open full size
1280 × 800 · 17.2 KB · 272 lines

Use it

Free to use · credit appreciated

1 Once, in your project

npx skills add SusanAcharya/Design-Lounge

2 Then ask

Using the Design Lounge skill, build "Glass credit card with flip" (piece card-glass-credit) 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.

Brief

257 lines

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 view of a wallet app for a fictional bank, “Vantor”. One 470px card made of frosted glass floats over three crisp shapes: a copper disc, a teal ring, and a sand slab. The glass blurs them, so the card reads as a real pane. It tilts up to 12° toward the pointer, a thin holographic band runs around its edge and rotates to face the pointer, and a soft glare follows. A flip shows the back with a magnetic stripe, a signature strip, and a CVV that stays masked unless you hold a button. A three-tab switcher swaps cards; the shapes behind the glass slide and recolour so every card has its own light. The detail worth copying is that the glass is real backdrop-filter, and the background shapes are crisp, not glowing blobs, so the blur has something to show.

Reference behaviour

  1. First frame: the Everyday card, front side, resting at rotateX 6° and rotateY −12° so it already looks dimensional. The copper disc is behind its upper left, the teal ring behind its lower right.
  2. Move the pointer anywhere on the page: the card eases toward rotateX −cy × 12°, rotateY cx × 14°, where cx, cy are the pointer position relative to the card centre, clamped to −1…1. Easing is a lerp of 0.12 per frame; the loop stops when it settles.
  3. The glare (radial-gradient at the pointer, soft-light) and the holographic edge band (conic-gradient whose start angle is the pointer’s angle from the card centre plus 30°) follow the pointer.
  4. Pointer leaves the document: the card eases back to the resting tilt.
  5. Click the card, press Enter or Space on it, or press “Show back”: the card flips 180° about Y in 700ms with a quartic ease-out. At 90° the front content is swapped for the back content, which is mirrored back to read correctly. The button text becomes “Show front”.
  6. Back side: a 46px dark stripe 26px from the top, a striped signature panel with “Anika Rai” in italic mono, a white CVV box showing ”•••”, and a 40px pill “Hold to reveal CVV” with a 28px progress ring.
  7. Press and hold the pill (pointer or Space/Enter): the ring fills over 500ms and the label reads “Keep holding…”. When full, the CVV shows “382” and the label reads “Release to hide”. Releasing, leaving, cancelling, or blurring masks it again immediately. Clicking the pill does not flip the card.
  8. Arrow keys on the focused card nudge the tilt 4° per press, within ±12° / ±14°.
  9. Pick “Travel” or “Vault” in the switcher (or ArrowLeft/ArrowRight between tabs): the card fades and drops 24px in 300ms, the body theme changes, the shapes slide to new positions and colours over 700ms, then the new card fades up. The back resets to the front. The balance line under the flip button changes.
  10. Reduced motion: no tilt, the flip is an instant swap, the CVV reveals without the ring delay, the switch is instant.

Structure

1280 × 800, page #0b1513, single centred column, gap 28px
                     VANTOR · WALLET            11px mono, 0.18em
                  Your cards · 3 active         Unbounded 22px
        ◯ copper disc           ▭ sand slab
          ┌──────────────────────────────────┐
          │ vantor EVERYDAY             )))  │  470 × 296 (ratio 1.586)
          │ [chip]                            │  radius 20px, blur 22px
          │                                   │  1.5px holographic edge
          │ •••• •••• ••••  4821              │
          │ CARD HOLDER  VALID THRU     ≡     │
          │ ANIKA RAI    09/29                │
          └──────────────────────────────────┘
                                 ◎ teal ring
                     ( ⟳ Show back )            40px pill
               Available NPR 84,210.50
      ┌──────────────────────────────────────────────┐
      │ [▭] Everyday ··4821 │ [▭] Travel ··0937 │ [▭] Vault ··5560 │  tablist
      └──────────────────────────────────────────────┘
  • main.wrap (flex column, overflow-x: clip so the shapes never cause sideways scroll).
  • header.head: kicker and h1.
  • .stage: width: 470px; aspect-ratio: 1.586, position: relative. Inside: three i.sh shapes (decorative) and div.card[role=group][tabindex=0][aria-roledescription="payment card"].
  • .card holds .face.front-f and .face.back-f. Only one is displayed at a time; the back is mirrored with scaleX(-1) while the card is past 90°.
  • .ctrls: the flip button, a polite balance paragraph, and div[role=tablist] with three button[role=tab], each with a 36×24 mini card swatch, a name, and the last four digits.
  • One visually hidden p[role=status].

Cards:

TabTypeLast 4ExpiryCVVBalance lineShapes s1 / s2 / s3
Everydaydebit482109/29382Available NPR 84,210.50#c9733f / #2fa39a / #e8d9b5
Travelcredit093703/28715Limit left USD 2,640.00 · no FX fee#f07457 / #1f6fb2 / #f2e3c6
Vaultcredit556011/30049Limit left NPR 412,000.00 · metal#d9b45a / #3a4340 / #ede6d6

Tokens

:root {
  /* colour */
  --bg: #0b1513;                     /* deep green-black page */
  --text: #eef2ec;                   /* all card text */
  --text-2: #a9b8b1;                 /* kicker, captions, inactive tabs */
  --line: rgba(238,242,236,.14);     /* pill and tablist borders */
  --s1: #c9733f;                     /* shape: disc */
  --s2: #2fa39a;                     /* shape: ring */
  --s3: #e8d9b5;                     /* shape: slab, focus ring, CVV progress */
  --tint: rgba(240,246,242,.10);     /* glass body tint, per card */

  /* type */
  --disp: "Unbounded", system-ui, sans-serif;
  --mono: "Sometype Mono", ui-monospace, monospace;

  /* geometry */
  --cw: 470px;                       /* card width; height from aspect-ratio 1.586 */
  --r-card: 20px; --r-pill: 999px; --r-tabs: 16px; --r-tab: 11px;
  --blur: 22px;

  /* space: 4px base */
  --s-1: 4px; --s-2: 8px; --s-3: 14px; --s-4: 18px; --s-5: 24px; --s-6: 28px;

  /* motion */
  --ease: cubic-bezier(.2, .7, .2, 1);
  --expo: cubic-bezier(.16, 1, .3, 1);
  --t-flip: 700ms;    /* JS-driven, ease-out quart */
  --t-swap: 300ms;    /* card out / in */
  --t-shapes: 700ms;  /* background shapes move and recolour */
  --t-hold: 500ms;    /* CVV hold */
  --lerp: .12;        /* tilt smoothing per frame */
}
body[data-c="1"] { --s1: #f07457; --s2: #1f6fb2; --s3: #f2e3c6; --tint: rgba(236,244,255,.10); }
body[data-c="2"] { --s1: #d9b45a; --s2: #3a4340; --s3: #ede6d6; --tint: rgba(20,22,21,.28); }

Glass recipe: background: linear-gradient(135deg, rgba(255,255,255,.16), rgba(255,255,255,.04)), var(--tint); backdrop-filter: blur(22px) saturate(1.5); box-shadow: inset 0 1px 0 rgba(255,255,255,.25), 0 30px 60px -20px rgba(0,0,0,.55). Text on glass gets text-shadow: 0 1px 3px rgba(5,10,9,.35).

Typography

RoleFamilySizeWeightTrackingCase
KickerSometype Mono11px4000.18emUPPER
Page titleUnbounded22px600 (suffix 400 in --text-2)-0.01emsentence
Card wordmark “vantor”Unbounded20px700-0.02emlower
Card kindSometype Mono10px4000.16emUPPER, 85% opacity
Card numberSometype Mono21px5000.12emnumerals, masked groups 75%
Field captionSometype Mono9px4000.18emUPPER, 75%
Field valueSometype Mono13px4000.06emUPPER
CVVSometype Mono16px5000.14emnumerals
Tab nameUnbounded12px6000Title
Tab digits, balanceSometype Mono11–12px400 / 5000.04–0.06emas written

Motion

ElementTriggerPropertyFrom → ToDurationEasingReduced motion
Card tiltpointer move / arrowsrotateX, rotateYcurrent → target (±12°, ±14°)per-frame lerp 0.12lerpoff
Glarepointer movegradient centrefollows pointerimmediate–static at 30% 20%
Edge bandpointer moveconic start anglefollows pointer angle + 30°immediate–static 210°
Flipclick, Enter, Space, buttonrotateY offset0 → 180° (and back)700ms1 − (1 − t)^4instant swap
Face swapflip passes 90°displayfront ↔ back––same
Card switchtabopacity, translate1, 0 → 0, 24px → 1, 0300ms each way--easeinstant
Shapestabtransform, colourper theme700ms--expo / --easeinstant
CVV ringholdstroke-dashoffset69.1 → 0500mslinearskipped

Perspective is inside the transform (perspective(1200px) rotateX() rotateY()), not on a parent, so no preserve-3d is needed and backdrop-filter keeps working.

States

  • Resting: tilt 6° / −12°, front visible.
  • Tilting: follows the pointer; edge band and glare track it.
  • Flipped (.flipped): back visible and mirrored; flip button reads “Show front”; tilt Y target inverts so the motion still follows the pointer.
  • Holding: pill .holding, ring filling, “Keep holding…”.
  • Revealed: CVV digits shown, “Release to hide”. Any release masks.
  • Switching (.out): card faded and 24px lower.
  • Tab selected: aria-selected="true", rgba(255,255,255,.1) fill, full-white text. Others --text-2; hover lifts them to --text.
  • Focus-visible: 2px --s3 outline, 3px offset; on the card the offset is 6px so it clears the edge band.
  • Disabled / frozen card (not drawn): drop the tint to rgba(20,22,21,.5), desaturate the shapes, and replace the hold pill with “Card frozen” text.

Accessibility

  • The card is role="group" with aria-roledescription="payment card" and a label that changes: “Everyday debit card ending 4821, front. Press Enter to flip.” It is focusable; Enter and Space flip; arrows tilt.
  • The full number is never in the DOM. The masked groups are aria-hidden; the label carries the last four.
  • The CVV box is aria-live="polite" and its aria-label switches between “Security code hidden” and “Security code 3 8 2” (spaced digits).
  • The hold pill is a real button with aria-describedby hint “Press and hold. The code hides again when you let go.” It works with pointer (with pointer capture) and with Space/Enter held (ignoring repeat).
  • The switcher is a tablist with roving tabindex; ArrowLeft/Right/Up/Down move and select.
  • A polite status region announces “Showing back of card” and “Travel card ending 0937 selected”.
  • Contrast: card text sits on blurred colour, so it gets a soft text-shadow and the glass a tint; check the lightest shape (sand) under the captions. --text on --bg 16:1, --text-2 on --bg 8.6:1.
  • Hit targets: flip pill 40px, hold pill 40px, tabs 48px.

Responsive rules

  • ≥ 1280: card 470px.
  • 1024 / 768: unchanged; the column is narrow by design.
  • < 520 (check 375): card 330px; card padding 16px 18px; chip 38×28; number 16px; wordmark 16px; back stripe 36px; fine print hidden; tablist stacks vertically at up to 330px wide.
  • Touch devices: tilt ignores pointerType === 'touch'; the card rests at its default tilt.
  • Shapes may overhang the stage; the wrapper clips horizontal overflow so the page never scrolls sideways.

Acceptance checklist

Always

  • The glass is a real backdrop-filter over crisp background shapes, not a blurred image or glowing blobs.
  • Tilt is clamped (±12° X, ±14° Y), smoothed, and stops its loop when settled.
  • The edge band and the glare both follow the pointer.
  • Flip works from click, Enter, Space, and a visible button, and swaps faces at 90°.
  • The CVV is masked by default, reveals only while held, and re-masks on release, blur, or cancel.
  • Holding uses pointer capture and keyboard keydown/keyup, ignoring key repeat.
  • The switcher is a tablist with roving tabindex and arrow keys; switching resets to the front.
  • The full card number never appears in the DOM.
  • Reduced motion removes tilt and animation but keeps flip, reveal, and switch.
  • No horizontal scroll at 375px.

This demo

  • Card 470px, ratio 1.586, radius 20px, blur 22px.
  • Everyday 4821 (09/29), Travel 0937 (03/28), Vault 5560 (11/30), holder Anika Rai.
  • Hold time 500ms; CVVs 382, 715, 049.
  • Page #0b1513; Everyday shapes copper #c9733f, teal #2fa39a, sand #e8d9b5.

Implementation notes

Holographic edge with a mask. One pseudo-element, a 1.5px padding, and a mask that removes the content box:

.card::before {
  content: ""; position: absolute; inset: 0; border-radius: inherit; padding: 1.5px; pointer-events: none;
  background: conic-gradient(from var(--a, 210deg),
    rgba(255,255,255,.14) 0deg, #fbe3b5 30deg, #a8f0de 60deg, #f7b8c8 90deg,
    rgba(255,255,255,.14) 130deg 360deg);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0);
}
.card::after {
  content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
  mix-blend-mode: soft-light;
  background: radial-gradient(circle at var(--gx, 30%) var(--gy, 20%), rgba(255,255,255,.55), transparent 45%);
}

One transform, flip included. Keep tilt and flip in the same rotateY so no 3D context is needed:

function apply() {
  card.style.setProperty('--rx', rx + 'deg');
  card.style.setProperty('--ry', (ry + flipA) + 'deg');   // flipA animates 0 ↔ 180
  card.classList.toggle('flipped', flipA > 90);            // .flipped .back-f { display:flex; transform: scaleX(-1) }
}
function loop() {
  rx += (trx - rx) * .12; ry += (try_ - ry) * .12;
  if (anim) { const t = Math.min(1, (performance.now() - anim.t0) / 700);
    flipA = anim.from + (anim.to - anim.from) * (1 - Math.pow(1 - t, 4)); if (t === 1) anim = null; }
  apply();
  if (anim || Math.abs(trx - rx) > .05 || Math.abs(try_ - ry) > .05) requestAnimationFrame(loop);
}

Hold, don’t click.

hold.addEventListener('pointerdown', e => { hold.setPointerCapture(e.pointerId); start(); });
hold.addEventListener('keydown', e => { if (!e.repeat && (e.key === ' ' || e.key === 'Enter')) { e.preventDefault(); start(); } });
['pointerup', 'pointercancel', 'lostpointercapture', 'keyup', 'blur'].forEach(t => hold.addEventListener(t, stop));
function start() { hold.classList.add('holding'); timer = setTimeout(reveal, 500); }
function stop()  { clearTimeout(timer); hold.classList.remove('holding'); mask(); }

Common mistakes:

  • Putting backdrop-filter inside a transform-style: preserve-3d tree. Chrome drops the blur. Use the single-element flip above.
  • Glass over a flat dark page. With nothing behind it, the blur shows nothing and the card looks grey.
  • Rainbow foil over the whole face. Here the colour lives on a 1.5px edge only.
  • Showing the CVV on click and leaving it up.
  • Rendering the full PAN in hidden text “for screen readers”.
  • Copying a real card network mark. The three bars are an invented mark.

Rebuild order:

  1. Page, header, stage with three shapes.
  2. Glass card front: wordmark, chip, number, fields, mark.
  3. Edge band and glare from custom properties.
  4. The rAF tilt loop with lerp and pointer mapping.
  5. JS-driven flip with face swap at 90°.
  6. Back face, hold-to-reveal with ring.
  7. Tablist switcher with shape themes and the out/in swap.
  8. Labels, live region, reduced motion, 520px breakpoint.

Details

Palette
Type
Unbounded · Sometype Mono
Motion
Rich motion
Build
Multi-step
Designed bySusan AcharyaKathmandu

Studied

The F-List

Make the reveal the message. Bars lift off the text, and badges explain the list.

All sources

On the shelves