All components

Cube Flip Cards

GSAP animation component · Published 2026-09-05 · by vanguardia.dev

Open live demo ↗ Raw prompt (.md)

What it does

Four cards built as six-faced prisms with frosted glass sides: hovering flips the prism 180° on its horizontal axis to reveal the identical back face, after which pointer movement tilts it on both axes with short overwriting tweens, and a specular highlight sweeps across the face with the rotation. Leaving returns it to rest.

How it's built

Categorycards
Techgsap
Complexitywidget
Performance costlight
Mobile-safeyes

cards 3d cube flip hover tilt glass perspective

Rebuild it with AI

To reproduce this animation in your own project, copy the prompt below into Claude Code, Cursor or any AI coding agent. The prompt is validated — it describes the exact structure, timing and easing, so the agent rebuilds the effect faithfully and you can then adapt colors, copy and layout to your design.

The full prompt

Cube Flip Cards — Six-Faced Glass Prisms that Flip on Hover and Tilt with the Pointer

Goal

Build a row of four cards, each rendered as a real 3D prism with six faces: front and back are frosted white glass carrying a thumbnail, a title, a short description and an arrow; the four sides are translucent. On hover the prism flips 180° on its horizontal axis (you see the top and bottom faces sweep past) and lands on its identical back face; from then on pointer movement tilts it on both axes (up to ±40°) with short tweens, and a specular highlight sweeps across the face with the rotation. On leave it eases back to rest. GSAP core.

Tech

Vanilla HTML/CSS/JS with ES module imports. gsap (npm). Font: DM Sans (400–500).

Layout / HTML

<div class="cf">
  <section class="cf-stage">
    <div class="cf-card"></div><div class="cf-card"></div><div class="cf-card"></div><div class="cf-card"></div>
  </section>
</div>

JS fills each .cf-card with:

<div class="cf-cube">
  <div class="cf-face cf-face--front">FACE</div>
  <div class="cf-face cf-face--back">FACE</div>
  <div class="cf-face cf-face--right"></div><div class="cf-face cf-face--left"></div>
  <div class="cf-face cf-face--top"></div><div class="cf-face cf-face--bottom"></div>
</div>

where FACE = <img class="cf-thumb">, <h2 class="cf-title">, <div class="cf-meta"><p class="cf-desc">…</p><svg class="cf-arrow">→</svg></div>. Data: "Flip me / Go on, give it a hover. I dare you.", "Spin cycle / Now with 100% more tumble per second.", "Peek-a-boo / Six faces, one attitude. See them all.", "Lean in / It follows your cursor. A little clingy, honestly."

Styling

:root: --cf-paper:#ebece9, --cf-ink:#16171a, --cf-face:rgba(255,255,255,.82), --cf-side:rgba(255,255,255,.35), --cf-line:rgba(255,255,255,.7), --cf-card-w:300px, --cf-card-h:400px, --cf-depth:340px.

  • .cf-stageposition:relative; min-height:100svh; display:flex; gap:3rem; align-items:center; justify-content:center; overflow:hidden; padding:2rem; ::before is a big soft white radial light (60vmax, opacity:.6) centred behind the cards.
  • .cf-cardwidth:var(--cf-card-w); height:var(--cf-card-h); perspective:2000px; flex-shrink:0.
  • .cf-cubeposition:relative; width:100%; height:100%; transform-style:preserve-3d; cursor:pointer; transform:translateZ(calc(var(--cf-depth) / -2)).
  • .cf-faceposition:absolute; backface-visibility:hidden; border:1px solid var(--cf-line).
  • front/back — width:var(--cf-card-w); height:var(--cf-card-h); display:flex; flex-direction:column; padding:1.5rem; background:var(--cf-face); backdrop-filter:blur(18px) saturate(1.3); box-shadow:inset 0 1px 0 rgba(255,255,255,.9). Front transform:translateZ(calc(var(--cf-depth)/2)), back rotateX(180deg) translateZ(calc(var(--cf-depth)/2)).
  • right/left — width:var(--cf-depth); height:var(--cf-card-h); left:calc((var(--cf-card-w) − var(--cf-depth))/2); background:var(--cf-side); right rotateY(90deg) translateZ(calc(var(--cf-card-w)/2)), left rotateY(−90deg) ….
  • top/bottom — width:var(--cf-card-w); height:var(--cf-depth); top:calc((var(--cf-card-h) − var(--cf-depth))/2); background:var(--cf-side); top rotateX(90deg) translateZ(calc(var(--cf-card-h)/2)), bottom rotateX(−90deg) ….
  • Specular sweep on front/back ::after: linear-gradient(115deg, transparent 30%, rgba(255,255,255,.55) 50%, transparent 70%), background-size:250% 100%, background-position: var(--cf-shine, 100%) 0, mix-blend-mode:screen; pointer-events:none.
  • .cf-thumbalign-self:flex-end; width:125px; height:155px; margin-bottom:auto; object-fit:cover; border-radius:6px. .cf-title2rem, 500, letter-spacing:-.03em. .cf-metadisplay:flex; align-items:flex-end; justify-content:space-between. .cf-desc.9rem, opacity:.75. .cf-arrow1.5rem square.
  • @media (max-width:1000px): --cf-card-w:260px; --cf-card-h:340px; --cf-depth:280px, stage becomes a column with padding:20svh 1rem.

GSAP effect (be exact)

Build in mount(config)destroy().

const DEFAULTS = { parallaxStrength: 40, flipDuration: .5, returnDuration: .6, tiltDuration: .6 };

Per cube read depth = parseFloat(getComputedStyle(wrapper).getPropertyValue("--cf-depth")) and keep rot = { flip: 0, tiltX: 0, tiltY: 0 }, flipped = false.

render()gsap.set(cube, { rotationX: rot.flip + rot.tiltX, rotationY: rot.tiltY, z: −depth / 2 }); then shine = 100 − (rot.flip / 180) * 100 − rot.tiltY * .6 and cube.style.setProperty("--cf-shine", shine + "%"). Call once at init.

  • mouseenterflipped = false; gsap.to(rot, { flip: 180, duration: flipDuration, ease: "power2.inOut", overwrite: "flip", onUpdate: render, onComplete: () => flipped = true }).
  • mouseleaveflipped = false; gsap.to(rot, { flip: 0, tiltX: 0, tiltY: 0, duration: returnDuration, ease: "power3.out", overwrite: true, onUpdate: render }).
  • mousemove (only when flipped) — ox = (clientX − centreX) / width, oy = (clientY − centreY) / height from the cube's bounds; gsap.to(rot, { tiltY: ox * parallaxStrength, tiltX: −oy * parallaxStrength, duration: tiltDuration, ease: "power2.out", overwrite: "tilt", onUpdate: render }).

overwrite:"flip" / "tilt" are overwrite *groups*: a new flip tween cancels the previous flip but not the tilt, and vice-versa; the leave tween uses overwrite:true to cancel both.

destroy() — remove listeners, kill tweens on the cubes, empty the wrappers.

Assets / images

Four product-style stills on flat pastel backgrounds (a vase, a lamp, books, a chair), portrait-cropped to 125×155 in the card. ~600px source is enough.

Behavior notes

  • The tilt only starts after the flip completes, so the first movement is always the clean 180° turn.
  • backface-visibility:hidden keeps the six faces from bleeding through each other during the flip; the sides use a translucent fill so the volume reads as glass rather than a box.
  • On touch devices there is no hover: the cards are static, fully readable fronts.
  • Only prefixed variables are global; the section is self-contained.

Images

This component ships with 4 reference assets, served publicly. Use them as-is to reproduce the demo faithfully, then swap in your own — the layout expects the same aspect ratios.

https://motionprompts.dev/c/cube-flip-cards/img1.jpg
https://motionprompts.dev/c/cube-flip-cards/img2.jpg
https://motionprompts.dev/c/cube-flip-cards/img3.jpg
https://motionprompts.dev/c/cube-flip-cards/img4.jpg

They are hotlinkable for prototyping. For anything you ship, replace them: they are licensed for demonstration of this component, not for redistribution.

Using this outside its demo page

This component is written as a complete page — that is how the demo is meant to look. If you are dropping it into an existing project, or combining it with other components, these are the things it declares at document level and that you need to move or reconcile first.

  • Palette on :root--cf-paper, --cf-ink, --cf-face, --cf-side, --cf-line, --cf-card-w, --cf-card-h, --cf-depth. These names are not namespaced and they collide: --ink is defined by 164 of the 219 components in this catalogue, --paper by 94, --muted by 80, each with different values — and they will also collide with whatever your own project defines. Move them onto the component's wrapper (.my-section { --ink: … }) or rename them with a prefix.
  • Rules on html, body — the demo owns the whole document, so these set the page background, typography and resets. Dropped into an existing project they restyle the entire page, not just this section. Re-target them at the component's wrapper before using it.