Aduok Code

Animated Flip Credit Card Form with Pure HTML & CSS

Introduction

In this article we build CardFlip — a polished animated credit card form from scratch. The card preview updates in real time as the user types, and flips to reveal the back whenever the CVV field is focused. No frameworks, no CSS preprocessors, no external UI libraries. Just HTML, CSS custom properties, keyframe animations, and a small amount of vanilla JavaScript for the input mirroring.

The component is built from two main regions: a 3-D card scene containing a front face and a back face, and a glassmorphism form panel below it. Every color, size, and timing token lives in CSS custom properties at :root, making the whole component trivially re-themeable. An animated aurora effect on the card surface is achieved with a single conic-gradient pseudo-element and one @keyframes rule.

What you will learn

How to set up a CSS 3-D perspective scene for a card flip — how to use backface-visibility: hidden to show only one face at a time — how to mirror live form input onto a card preview with vanilla JS — how to create an animated conic-gradient aurora with a single keyframe — how to build a glassmorphism form panel with CSS backdrop-filter — how to structure design tokens with CSS custom properties for a fully themeable component.

How the Flip Works

The flip effect relies on three CSS properties working together: perspective on the scene container, transform-style: preserve-3d on the card inner wrapper, and backface-visibility: hidden on each face. At rest, the front face is visible. When the .flipped class is added to the inner wrapper, it rotates 180deg around the Y axis. The back face starts pre-rotated at 180deg, so it comes into view exactly as the front disappears.

The flip is just one CSS class toggle — .flipped adds transform: rotateY(180deg) to the inner wrapper. JavaScript adds and removes that class on CVV focus and blur.

This is why backface-visibility: hidden is essential on both faces. Without it, the back face would show through the front face as a mirror image during the rotation — breaking the illusion entirely.

01
Part One
Design Tokens

Step 1 — CSS Custom Properties

All values are declared as custom properties on :root. Tokens are grouped into palette, geometry, and motion categories. This makes the component self-documenting and trivial to re-theme by changing only the :root block.

CSS
:root {
  /* palette */
  --color-bg:           #0f0f13;
  --color-card-from:    #1a1a2e;
  --color-card-mid:     #16213e;
  --color-card-to:      #0f3460;
  --color-accent:       #a855f7;
  --color-accent-deep:  #7c3aed;
  --color-pink:         #ec4899;
  --color-blue:         #3b82f6;
  --color-text:         rgba(255, 255, 255, 0.9);
  --color-text-muted:   rgba(255, 255, 255, 0.5);
  --color-form-bg:      rgba(255, 255, 255, 0.04);
  --color-input-bg:     rgba(255, 255, 255, 0.06);
  --color-border:       rgba(255, 255, 255, 0.08);
  --color-border-input: rgba(255, 255, 255, 0.1);

  /* geometry */
  --card-width:         380px;
  --card-height:        240px;
  --card-radius:        20px;
  --perspective:        1000px;

  /* motion */
  --duration-flip:      0.7s;
  --easing-flip:        cubic-bezier(0.4, 0, 0.2, 1);
  --duration-aurora:    8s;
  --duration-blob:      12s;
}
Why separate card colors from UI colors?

Keeping card palette tokens (--color-card-*) separate from form palette tokens (--color-form-bg, --color-input-bg) means you can re-skin just the card — for example swapping to a gold luxury theme — without touching the form styles at all.

02
Part Two
HTML Structure

Step 2 — The Markup

The component splits into two top-level sections: .card-scene and .form-card. Inside the scene, .card-inner is the 3-D pivot element that holds .card-front and .card-back as absolute children. The form panel below contains four field groups — card number, card holder, expiry selects, and CVV.

HTML
<div class="wrapper">

  <!-- 3-D card scene -->
  <div class="card-scene">
    <div class="card-inner" id="cardInner">

      <div class="card-front">
        <div class="card-top">...</div>
        <div class="card-number-display" id="cardNumDisplay">
          <span id="g1">####</span>
          <span id="g2">####</span>
          <span id="g3">####</span>
          <span id="g4">####</span>
        </div>
        <div class="card-bottom">
          <div class="card-info-value" id="holderDisplay">FULL NAME</div>
          <div class="card-info-value" id="expiryDisplay">MM/YY</div>
        </div>
      </div>

      <div class="card-back">
        <div class="magnetic-stripe"></div>
        <div class="cvv-section">
          <div class="cvv-band">
            <span class="cvv-dots" id="cvvDisplay">***</span>
          </div>
        </div>
      </div>

    </div>
  </div>

  <!-- Form panel -->
  <div class="form-card">
    <input id="cardNumber" type="text" autocomplete="off" />
    <input id="cardHolder" type="text" autocomplete="off" />
    <select id="expMonth">...</select>
    <select id="expYear">...</select>
    <input id="cvv" type="password" autocomplete="off" />
    <button>Pay Now</button>
  </div>

</div>
ElementRoleAnimated
.card-scenePerspective container — sets the 3-D viewportNo
.card-inner3-D pivot — receives .flipped class on CVV focusYes — rotateY(180deg)
.card-frontFront face — visible by defaultNo
.card-backBack face — pre-rotated 180deg, visible when flippedNo
#cardNumDisplayLive card number — four grouped spansNo
#holderDisplayLive cardholder nameNo
#expiryDisplayLive expiry dateNo
.magnetic-stripeDecorative black stripe on card backNo
#cvvDisplayMasked CVV dots on card backNo
.card-front::beforeAurora conic-gradient overlayYes — aurora 8s
03
Part Three
The 3-D Scene

Step 3 — Setting Up the Perspective

Three CSS properties chain together to make the flip work. perspective on .card-scene defines the vanishing point — 1000px gives a realistic depth without too much distortion. transform-style: preserve-3d on .card-inner tells the browser to keep children in 3-D space rather than flattening them. backface-visibility: hidden on both faces hides them when they face away from the viewer.

CSS
.card-scene {
  width: var(--card-width);
  height: var(--card-height);
  perspective: var(--perspective);
  margin: 0 auto 32px;
}

.card-inner {
  width: 100%;
  height: 100%;
  position: relative;
  transform-style: preserve-3d;
  transition: transform var(--duration-flip) var(--easing-flip);
}

/* Class toggled by JS on CVV focus / blur */
.card-inner.flipped {
  transform: rotateY(180deg);
}

.card-front,
.card-back {
  position: absolute;
  inset: 0;
  border-radius: var(--card-radius);
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden; /* Safari */
  overflow: hidden;
}

/* Back face starts rotated — comes into view when .flipped */
.card-back {
  transform: rotateY(180deg);
}
Why -webkit-backface-visibility?

Safari still requires the -webkit- prefix for backface-visibility. Without it, both faces are visible simultaneously in Safari during the rotation, producing a ghost double-image. Always include both the prefixed and unprefixed declarations.

04
Part Four
Card Front

Step 4 — Building the Front Face

The card front uses a three-stop linear-gradient as its base background, giving a deep navy-to-dark-blue ramp. The card is divided into three vertical zones via flexbox column layout: a top row for branding and the Mastercard logo, a middle row for the card number, and a bottom row for holder name and expiry.

CSS
.card-front {
  background: linear-gradient(
    135deg,
    var(--color-card-from) 0%,
    var(--color-card-mid)  40%,
    var(--color-card-to)   100%
  );
  padding: 28px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  box-shadow:
    0 30px 60px rgba(0, 0, 0, 0.5),
    0 0 0 1px rgba(255, 255, 255, 0.05);
}

/* Number display — single flex row, monospace font */
.card-number-display {
  font-family: 'Space Mono', monospace;
  font-size: 15px;
  letter-spacing: 2px;
  color: var(--color-text);
  white-space: nowrap;
  display: flex;
  gap: 10px;
  align-items: center;
  position: relative;
  z-index: 1;
}

/* Mastercard logo — two overlapping circles via absolute positioning */
.mastercard-logo {
  width: 54px;
  height: 36px;
  position: relative;
}
.mc-circle {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  position: absolute;
}
.mc-left  { background: #eb001b; left: 0;  }
.mc-right { background: #f79e1b; left: 18px; }

The gold chip is a div with a linear-gradient background. Its internal grid lines — the horizontal and vertical separator lines visible on real EMV chips — are faked with ::before and ::after pseudo-elements set to 1px height/width with a semi-transparent dark color.

05
Part Five
Card Back & CVV

Step 5 — The Back Face

The card back shares the same gradient background as the front, and the same aurora animation. Two additional elements appear only on the back: .magnetic-stripe — a full-width absolute-positioned div spanning the top third of the card — and .cvv-section containing the white CVV band.

CSS
.magnetic-stripe {
  position: absolute;
  top: 48px;
  left: 0;
  right: 0;
  height: 52px;
  background: linear-gradient(
    180deg,
    #111 0%,
    #222 50%,
    #111 100%
  );
  z-index: 1;
}

.cvv-band {
  background: rgba(255, 255, 255, 0.92);
  border-radius: 4px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding: 0 14px;
}

.cvv-dots {
  font-family: 'Space Mono', monospace;
  font-size: 16px;
  color: #111;
  letter-spacing: 4px;
}

The CVV display shows one asterisk per digit typed — "*".repeat(val.length) in JavaScript — falling back to "***" when the field is empty. This gives users real-time visual confirmation that their input is being captured, without ever exposing the actual digits.

06
Part Six
Aurora Animation

Step 6 — The Rotating Aurora Effect

The aurora is a ::before pseudo-element on both card faces. It is sized to 200% × 200% and positioned at −50% top and left, so it always fully covers the card regardless of rotation. The background is a conic-gradient with four color stops producing a sweeping purple-blue-pink light that slowly rotates behind the card surface.

CSS
.card-front::before,
.card-back::before {
  content: '';
  position: absolute;
  width: 200%;
  height: 200%;
  top: -50%;
  left: -50%;
  background: conic-gradient(
    from 0deg at 50% 50%,
    transparent              0deg,
    rgba(168, 85, 247, 0.3)  60deg,
    rgba(59, 130, 246, 0.2)  120deg,
    transparent              180deg,
    rgba(236, 72, 153, 0.2)  240deg,
    transparent              300deg
  );
  animation: aurora var(--duration-aurora) linear infinite;
}

@keyframes aurora {
  from { transform: rotate(0deg);   }
  to   { transform: rotate(360deg); }
}
Why 200% width and height?

A conic-gradient rotates around its center point. If the pseudo-element were only 100% × 100%, the corners of the card would clip the rotating gradient as it spins, causing a visible edge sweep. Sizing it to 200% and centering it at −50% ensures the gradient always fully covers the card with smooth edges at every rotation angle.

07
Part Seven
The Form

Step 7 — Glassmorphism Form Panel

The form panel uses backdrop-filter: blur(20px) on a semi-transparent dark background to create a glassmorphism effect that layers naturally over the animated background blobs. Input fields share a common style: semi-transparent background, subtle border, and a purple glow on focus via box-shadow.

CSS
.form-card {
  background: var(--color-form-bg);
  border: 1px solid var(--color-border);
  border-radius: 24px;
  padding: 32px;
  backdrop-filter: blur(20px);
}

input, select {
  width: 100%;
  background: var(--color-input-bg);
  border: 1px solid var(--color-border-input);
  border-radius: 12px;
  padding: 14px 16px;
  font-size: 15px;
  color: #fff;
  outline: none;
  transition: border-color 0.2s, background 0.2s, box-shadow 0.2s;
  -webkit-appearance: none;
  autocomplete: off;
}

input:focus, select:focus {
  border-color: rgba(168, 85, 247, 0.6);
  background: rgba(168, 85, 247, 0.08);
  box-shadow: 0 0 0 3px rgba(168, 85, 247, 0.12);
}

/* CVV gets a pink accent instead of purple */
.cvv-field input:focus {
  border-color: rgba(236, 72, 153, 0.7);
  background: rgba(236, 72, 153, 0.08);
  box-shadow: 0 0 0 3px rgba(236, 72, 153, 0.12);
}

The expiry and CVV fields share a three-column grid row. Using grid-template-columns: 1fr 1fr 1fr keeps the month and year selects equal width while giving the CVV field the same proportional space — no magic pixel values required.

08
Part Eight
Live Input Mirroring

Step 8 — Mirroring Inputs onto the Card

The JavaScript is intentionally minimal — four event listeners, one class toggle. Each input field has an input or change event that reads the current value and writes it to the corresponding display element on the card. The card number listener also formats the raw digit string into grouped segments for display.

JavaScript
// Card Number — format into four groups, update four spans
document.getElementById('cardNumber').addEventListener('input', function () {
  const val = this.value.replace(/\D/g, '').slice(0, 16);

  // Re-format input field with spaces
  this.value = val.replace(/(.{4})/g, '$1 ').trim();

  // Update each span with actual digits or # placeholders
  const raw = [val.slice(0,4), val.slice(4,8), val.slice(8,12), val.slice(12,16)];
  ['g1','g2','g3','g4'].forEach((id, i) => {
    document.getElementById(id).textContent = raw[i].padEnd(4, '#');
  });
});

// Card Holder — uppercase mirror
document.getElementById('cardHolder').addEventListener('input', function () {
  document.getElementById('holderDisplay').textContent =
    this.value.toUpperCase() || 'FULL NAME';
});

// Expiry — read both selects, combine
function updateExpiry() {
  const m = document.getElementById('expMonth').value;
  const y = document.getElementById('expYear').value;
  document.getElementById('expiryDisplay').textContent =
    (m || 'MM') + '/' + (y || 'YY');
}
document.getElementById('expMonth').addEventListener('change', updateExpiry);
document.getElementById('expYear').addEventListener('change', updateExpiry);

// CVV — flip card on focus, restore on blur, mask digits
const cvvInput = document.getElementById('cvv');
const cardInner = document.getElementById('cardInner');

cvvInput.addEventListener('focus', () => cardInner.classList.add('flipped'));
cvvInput.addEventListener('blur',  () => cardInner.classList.remove('flipped'));
cvvInput.addEventListener('input', function () {
  const val = this.value.replace(/\D/g, '').slice(0, 4);
  this.value = val;
  document.getElementById('cvvDisplay').textContent =
    '*'.repeat(val.length) || '***';
});
Why autocomplete="off" on every input?

Browser autofill can silently populate payment fields with saved card data before the user interacts with them, bypassing the live mirroring logic entirely. Setting autocomplete="off" on all inputs ensures the card preview always reflects only what the user actively types — not what the browser pre-filled.

The card number display pads each group with # characters using padEnd(4, "#") — so partial input like "123" shows as "123#" rather than a mis-aligned gap.

Tuning Reference

Token / PropertyDefaultEffect
--duration-flip0.7sCard flip speed. Lower = snappier. Above 1s feels sluggish
--easing-flipcubic-bezier(0.4,0,0.2,1)Flip easing. Try `ease-in-out` for a simpler curve
--perspective1000pxLower = more dramatic distortion. Higher = flatter flip
--duration-aurora8sAurora rotation speed. Higher = slower, more subtle glow
--duration-blob12sBackground blob drift speed. Purely decorative
--card-radius20pxCard corner radius. 0 = sharp corporate look
--color-accent#a855f7Primary focus ring and button gradient color
--color-pink#ec4899CVV field focus ring color — intentionally distinct
backface-visibilityhiddenMust be hidden on both faces or ghost image appears
-webkit-backface-visibilityhiddenRequired for Safari — do not omit
conic-gradient stops0/60/120/180/240/300degAurora color sweep width. More stops = richer glow

Full Source Code

Save the following as animated-credit-card-form.html and open in any modern browser. Zero dependencies, zero build step.

HTML — animated-credit-card-form.html (complete)
// Full source available in the Aduok GitHub repository
// https://github.com/aduok/aduok-code-snippets/blob/main/blog/animated-credit-card-form.html

Every effect in CardFlip — the flip, the aurora, the glassmorphism form — is pure CSS. JavaScript does only one thing: move text from inputs to the card display.

Want to see it in action?Watch the full build on YouTube