Pure CSS Products & branding #hover #product #card #flip #branding

Business card in pure CSS

A thick business card with painted edges. Hover or focus and it flips to show the back; the logo and the text are lifted off the paper with translateZ, so they shift against it as it turns.

  • 213 lines of CSS
  • 20 lines of HTML
  • No JavaScript
  • No dependencies
  • Snippet under MIT
GitHub

How it works

  1. The card has real thickness: the printed front and back are two planes at translateZ(±2 units), the back turned with rotateY(180deg). Both hide their backface, so each shows only while it faces you.
  2. The painted edge is four rounded slabs 1 unit apart (they fill the rounded corners) plus four flat walls on the straight parts, which give the edge a clean, lit surface when the card is edge-on.
  3. Embossing is real depth: each printed side is itself preserve-3d, and the logo and the lines of text are lifted off it with translateZ(1.5–6 units). As the card turns, they slide against the paper.
  4. The idle sway and the flip are on two different elements, so they never fight over transform. The flip is a plain transition with a small overshoot.
  5. The hovered element is a static wrapper; the moving card inside has pointer-events: none, so the flip cannot pull the card out from under the pointer.
  6. Every length is a multiple of one base unit, --u, tied to the canvas, so the card is the same share of a gallery card, the editor and a recording canvas.

Key techniques

  • two faces at ±depth/2, back turned 180deg
  • rounded slabs + walls for the edge
  • embossing = children lifted with translateZ
  • flip and idle sway on separate elements

Copy-paste code

HTML 20 lines

<div class="scene">
  <div class="bizcard" tabindex="0">
    <div class="float">
      <i class="shadow"></i>
      <div class="card">
        <i class="slab" style="--i:0"></i><i class="slab" style="--i:1"></i><i class="slab" style="--i:2"></i><i class="slab" style="--i:3"></i>
        <i class="wall wall-t"></i><i class="wall wall-b"></i><i class="wall wall-l"></i><i class="wall wall-r"></i>
        <div class="front">
          <b class="logo"><i></i><i></i><i></i></b>
          <strong>PRISM</strong><small>DESIGN STUDIO</small>
        </div>
        <div class="back">
          <b></b>
          <strong>Alex Morgan</strong><small>Creative director</small>
          <span>alex@prism.studio</span><span>+1 555 0142</span><span>prism.studio</span>
        </div>
      </div>
    </div>
  </div>
</div>

CSS 213 lines

/* how far the card is flipped: 0 = front, 1 = back (registered, so it can transition) */
@property --flip {
  syntax: '<number>';
  inherits: true;
  initial-value: 0;
}

.scene {
  /* one base unit: every length below is a multiple of it, so the card is the same share of a
     card, the editor, a full screen and a recording canvas */
  --u: 0.4vmin;
  perspective: calc(800 * var(--u));
}

/* the static hit area */
.bizcard {
  --ink: #0f1020;
  --edge: #ff4d9d;
  --foil: #ffd291;
  --foil-deep: #aa6c16;
  display: grid;
  place-items: center;
  width: calc(220 * var(--u));
  height: calc(170 * var(--u));
  border-radius: calc(16 * var(--u));
  cursor: pointer;
  font-family: Inter, system-ui, sans-serif;
  transform-style: preserve-3d;
}

/* the tilt and a slow idle sway */
.float {
  position: relative;
  width: calc(176 * var(--u));
  height: calc(100 * var(--u));
  pointer-events: none;
  transform-style: preserve-3d;
  animation: float 6s ease-in-out infinite alternate;
}

.shadow {
  position: absolute;
  inset: calc(10 * var(--u)) calc(-6 * var(--u)) calc(-16 * var(--u));
  border-radius: 50%;
  background: radial-gradient(closest-side, rgb(0 0 0 / 0.38), transparent);
  transform: translateZ(calc(-36 * var(--u)));
}

/* the flip, on its own element */
.card {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  transform: rotateY(calc(var(--flip) * 180deg));
  /* quick through the side-on middle (where thin layers flicker), then overshoot and settle */
  transition: --flip 0.9s linear(0, 0.03 18%, 0.5 42%, 0.97 60%, 1.05 74%, 1);
}

.bizcard:hover .card,
.bizcard:focus-visible .card {
  --flip: 1;
}

/* 4 units thick: four rounded slabs fill the corners of the edge... */
.slab {
  position: absolute;
  inset: calc(0.5 * var(--u)); /* at rest the slabs draw the edge: they face you, so it stays one clean line */
  border-radius: calc(6 * var(--u));
  background: var(--edge);
  transform: translateZ(calc((var(--i) - 1.5) * 1 * var(--u)));
}

/* ...four walls give the straight parts a lit surface (inset by the 6 units radius) */
/* the walls only show near side-on (mid-flip); at rest a thin wall seen side-on breaks into dashes */
.wall {
  position: absolute;
  opacity: clamp(0, 1 - pow(var(--flip) - 0.5, 2) * 40, 1);
  background: linear-gradient(90deg, #b3366e, var(--edge) 50%, #ff82ba);
}

.wall-t, .wall-b { left: calc(6 * var(--u)); top: calc(50% - 2 * var(--u)); width: calc(164 * var(--u)); height: calc(4 * var(--u)); }
.wall-l, .wall-r { top: calc(6 * var(--u)); left: calc(50% - 2 * var(--u)); width: calc(4 * var(--u)); height: calc(88 * var(--u)); }
.wall-t { transform: rotateX(90deg) translateZ(calc(50 * var(--u))); }
.wall-b { transform: rotateX(-90deg) translateZ(calc(50 * var(--u))); }
.wall-l { transform: rotateY(-90deg) translateZ(calc(88 * var(--u))); }
.wall-r { transform: rotateY(90deg) translateZ(calc(88 * var(--u))); }

/* each printed side is 3D itself, so its print can be lifted off it */
.front,
.back {
  position: absolute;
  inset: 0;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  border-radius: calc(6 * var(--u));
  box-shadow: inset 0 0 0 calc(1 * var(--u)) rgb(255 255 255 / 0.24); /* a faint light rim: the outline reads smooth */
  transform-style: preserve-3d;
  backface-visibility: hidden;
}

.front > *,
.back > * {
  backface-visibility: hidden;
  /* the lifted print fades out while the card is side-on (--flip near 0.5) */
  opacity: clamp(0, pow(var(--flip) - 0.5, 2) * 60, 1);
}

.front {
  align-items: center;
  justify-content: center;
  gap: calc(3 * var(--u));
  background:
    radial-gradient(circle at 50% 38%, rgb(139 108 255 / 0.38), transparent 55%),
    repeating-linear-gradient(135deg, rgb(255 255 255 / 0.035) 0 calc(1 * var(--u)), transparent calc(1 * var(--u)) calc(6 * var(--u))),
    var(--ink);
  transform: translateZ(calc(2 * var(--u)));
}

.front strong {
  margin-top: calc(6 * var(--u));
  color: var(--foil);
  font-size: calc(13 * var(--u));
  font-weight: 800;
  letter-spacing: calc(5 * var(--u));
  text-indent: calc(5 * var(--u)); /* balances the space after the last letter */
  text-shadow: 0 calc(1 * var(--u)) 0 var(--foil-deep);
  transform: translateZ(calc(3 * var(--u)));
}

.front small {
  color: rgb(255 255 255 / 0.6);
  font-size: calc(5.5 * var(--u));
  font-weight: 600;
  letter-spacing: calc(2.5 * var(--u));
  text-indent: calc(2.5 * var(--u));
  transform: translateZ(calc(1.5 * var(--u)));
}

/* foil prism logo, lifted the most */
.logo {
  position: relative;
  width: calc(34 * var(--u));
  height: calc(30 * var(--u));
  transform-style: preserve-3d;
  transform: translateZ(calc(6 * var(--u)));
}

.logo i {
  position: absolute;
  clip-path: polygon(50% 0, 100% 100%, 0 100%);
}

.logo i:nth-child(1) { inset: 0; background: linear-gradient(135deg, #fff6dc, var(--foil) 40%, var(--foil-deep)); }
.logo i:nth-child(2) { inset: calc(9 * var(--u)) calc(7.5 * var(--u)) calc(3 * var(--u)); background: var(--ink); }
.logo i:nth-child(3) {
  top: calc(15 * var(--u));
  left: calc(30 * var(--u));
  width: calc(26 * var(--u));
  height: calc(9 * var(--u));
  clip-path: polygon(0 40%, 100% 0, 100% 100%, 0 60%); /* the spectrum fanning out */
  background: linear-gradient(#ff4d9d 0 33%, #ffb547 0 66%, #2ee6d6 0);
}

.back {
  justify-content: center;
  padding: calc(12 * var(--u)) calc(14 * var(--u));
  color: #fff;
  background:
    /* deep enough for the white details (4.5:1 and more): the bright #8b6cff to #c55dcb was 3.7:1 */
    radial-gradient(circle at 100% 0%, rgb(255 181 71 / 0.35), transparent 50%),
    linear-gradient(135deg, #6a45f5, #a3339f);
  transform: rotateY(180deg) translateZ(calc(2 * var(--u)));
}

.back strong { font-size: calc(13 * var(--u)); font-weight: 800; line-height: 1; text-shadow: 0 calc(1 * var(--u)) 0 rgb(0 0 0 / 0.25); transform: translateZ(calc(3 * var(--u))); }
.back small { margin: calc(3 * var(--u)) 0 calc(9 * var(--u)); font-size: calc(6.5 * var(--u)); font-weight: 600; letter-spacing: calc(1.5 * var(--u)); text-transform: uppercase; opacity: 0.85; transform: translateZ(calc(2 * var(--u))); }

.back span {
  display: flex;
  align-items: center;
  gap: calc(5 * var(--u));
  margin-top: calc(3 * var(--u));
  font-size: calc(6.5 * var(--u));
  font-weight: 500;
  transform: translateZ(calc(1.5 * var(--u)));
}

.back span::before {
  content: '';
  width: calc(4 * var(--u));
  height: calc(4 * var(--u));
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 0 0 calc(1.5 * var(--u)) rgb(255 77 157 / 0.6);
}

/* the small mark in the corner */
.back b {
  position: absolute;
  top: calc(12 * var(--u));
  right: calc(14 * var(--u));
  width: calc(18 * var(--u));
  height: calc(16 * var(--u));
  clip-path: polygon(50% 0, 100% 100%, 0 100%);
  background: linear-gradient(135deg, #fff, rgb(255 255 255 / 0.55));
  transform: translateZ(calc(4 * var(--u)));
}

@keyframes float {
  from { transform: translateY(calc(-3 * var(--u))) rotateX(16deg) rotateY(-20deg) rotateZ(-5deg); }
  to   { transform: translateY(calc(3 * var(--u))) rotateX(10deg) rotateY(-8deg) rotateZ(-3deg); }
}
Standalone snippet · plain CSS, no build step, no dependencies.