October DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsSlow PC?RecommendedPC slow today? Run a repair scan before it gets worseResolve common Windows issues and optimize system performance.Scan NowOctober DealsAmazon USDeal season is back - check today's better picksAmazon US: current deals, useful picks and tech finds.See Picks×
Skip to content
World desk6 min

How to Build a 3D Tilt Profile Card with HTML, CSS & JavaScript

Build a pointer-driven 3D tilt profile card with perspective, layered depth, clamped angles, smooth reset and a reduced-motion fallback, plus fixes for common flattening bugs.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A 3D tilt card needs three pieces: a wrapper that supplies perspective, a card that rotates with rotateX() and rotateY(), and a few lines of JavaScript that turn pointer position into bounded angles. This guide builds a complete profile card with raised layers, a smooth reset, and a reduced-motion fallback, and explains the CSS rules that most often break the effect.

How the effect works

CSS transforms can rotate and translate elements in 3D space, but 3D only looks like 3D once perspective is configured. A smaller perspective distance produces a stronger, more dramatic effect; a larger one looks subtler and flatter. The tilt itself is nothing more than a rotation around the X and Y axes that JavaScript updates as the pointer moves.

The code below is a standard implementation pattern. It has not been benchmarked here, so test it in the browsers and input types you target.

Step 1: Write the HTML

Use ordinary semantic markup. The card should read perfectly well with no effect applied.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
<div class="scene">
  <article class="card" aria-label="Profile card">
    <div class="card-bg"></div>
    <img class="avatar" src="avatar.jpg" alt="Portrait of Ada Rivera" width="120" height="120">
    <div class="info">
      <h2>Ada Rivera</h2>
      <p>Front-end engineer</p>
      <a class="btn" href="/contact">Get in touch</a>
    </div>
  </article>
</div>

Replace the name, image and link with your own. The background lives in its own element so that clipping (rounded corners) can be applied there rather than on the card, for a reason explained in the troubleshooting section.

Step 2: Add the CSS

.scene {
  display: grid;
  place-items: center;
  min-height: 100vh;
  perspective: 900px;
}

.card {
  --rx: 0deg;
  --ry: 0deg;
  position: relative;
  width: 320px;
  padding: 2rem;
  text-align: center;
  color: #fff;
  transform-style: preserve-3d;
  transform: rotateX(var(--rx)) rotateY(var(--ry));
  transition: transform 0.4s ease-out;
  will-change: transform;
}

.card.is-active { transition: transform 0.08s linear; }

.card-bg {
  position: absolute;
  inset: 0;
  border-radius: 20px;
  background: linear-gradient(145deg, #3b4cca, #1b1f4b);
  box-shadow: 0 20px 40px rgb(0 0 0 / 0.35);
  overflow: hidden;
}

.avatar {
  position: relative;
  border-radius: 50%;
  transform: translateZ(60px);
}

.info { position: relative; transform: translateZ(35px); }

.btn {
  display: inline-block;
  margin-top: 1rem;
  padding: 0.6rem 1.2rem;
  border-radius: 999px;
  background: #fff;
  color: #1b1f4b;
  text-decoration: none;
  transform: translateZ(20px);
}

What each key line does

  • perspective on .scene gives the child its depth cue. Placing it on the parent is one of two approaches (see the comparison below).
  • transform-style: preserve-3d on .card keeps the avatar, text and button positioned in 3D space instead of being flattened into the card’s plane. MDN describes the property as setting whether an element’s children are positioned in 3D space or flattened in the element’s plane.
  • translateZ() on the layers pushes foreground items toward the viewer, so they shift relative to the background as the card rotates. That parallax is what sells the depth.
  • Two transition speeds: a short one while the pointer is moving keeps tracking responsive, and a longer ease-out on release lets the card settle back smoothly.

Step 3: Add the JavaScript

const card = document.querySelector('.card');
const scene = document.querySelector('.scene');
const MAX_TILT = 12; // degrees; tune to taste

const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
let frame = null;

function clamp(n, min, max) {
  return Math.min(max, Math.max(min, n));
}

function onMove(e) {
  if (reduceMotion.matches || e.pointerType === 'touch') return;

  const rect = card.getBoundingClientRect();
  // Position within the card, from -0.5 (left/top) to +0.5 (right/bottom)
  const x = clamp((e.clientX - rect.left) / rect.width - 0.5, -0.5, 0.5);
  const y = clamp((e.clientY - rect.top) / rect.height - 0.5, -0.5, 0.5);

  cancelAnimationFrame(frame);
  frame = requestAnimationFrame(() => {
    card.classList.add('is-active');
    card.style.setProperty('--ry', `${x * 2 * MAX_TILT}deg`);
    card.style.setProperty('--rx', `${-y * 2 * MAX_TILT}deg`);
  });
}

function reset() {
  cancelAnimationFrame(frame);
  card.classList.remove('is-active');
  card.style.setProperty('--rx', '0deg');
  card.style.setProperty('--ry', '0deg');
}

scene.addEventListener('pointermove', onMove);
scene.addEventListener('pointerleave', reset);
card.addEventListener('blur', reset);

Why the math looks like this

  • Moving right should turn the right edge away from you, which is a positive rotateY. Moving down should lift the bottom edge toward you, which is a negative rotateX. That is why the Y value is negated.
  • Normalizing to ±0.5 and multiplying by 2 gives a range of ±1, so the final angle never exceeds MAX_TILT. Clamping guards against pointer positions just outside the card.
  • Writing CSS custom properties keeps all visual logic in the stylesheet; JavaScript only supplies two numbers.
  • requestAnimationFrame limits style updates to one per frame even if the pointer fires events faster.
  • Listening on .scene rather than .card avoids a feedback jitter: when a rotated card’s edge moves out from under a stationary pointer, pointerleave on the card itself can fire and snap it back, then re-enter, repeatedly. The larger scene area is stable.

Choosing where perspective goes

Approach Where it goes Best for
perspective property On the parent of the transformed element (.scene) Layered cards; the vanishing point stays fixed while the card rotates inside it
perspective() function First item in the card’s own transform list, e.g. transform: perspective(900px) rotateX(...) Self-contained single elements, or several cards that should each have their own vanishing point

These are related but not interchangeable: the function belongs in the transformed element’s transform list, while the perspective and perspective-origin properties belong on the parent of a child transformed in 3D. If you use the function, it must come before the rotations in the list, because transform order matters.

Rank #2
Sale
HTML and CSS: Design and Build Websites
  • HTML CSS Design and Build Web Sites
  • Comes with secure packaging
  • It can be a gift option

Troubleshooting: the layers look flat

The most common failure is that the card tilts but the avatar and text do not pop out. Two causes account for most cases.

1. preserve-3d is missing on an intermediate element

transform-style is not inherited. If you wrap the avatar in an extra div inside the card, that wrapper also needs transform-style: preserve-3d, or its children are flattened.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

2. An element has a flattening property

Certain values force the flattened result even when preserve-3d is set. MDN lists, among others, opacity below 1, a filter other than none, and overflow other than visible or clip. Inspect every ancestor between the scene and the layers. This is why the example puts overflow: hidden on .card-bg, a sibling with no 3D children, and not on .card. If you want a fade-in, animate it on .scene or its parent instead of the card, and verify the result.

Respecting motion preferences

MDN recommends considering prefers-reduced-motion to turn off animations and, where appropriate, offering a control to disable motion. The script above already skips tilting when the preference is set. Add a CSS guard so the transition and any residual transforms are also removed:

Rank #4
Sale
Web Design with HTML, CSS, JavaScript and jQuery Set
  • Brand: Wiley
  • Set of 2 Volumes
  • A handy two-book set that uniquely combines related technologies Highly visual format and accessible language makes these books highly effective learning tools Perfect for beginning web designers and front-end developers
@media (prefers-reduced-motion: reduce) {
  .card {
    transition: none;
    transform: none;
  }
}

With transform: none on the card, the child translateZ layers collapse into a plain, static card, which is the correct fallback. If the user changes the OS setting while the page is open, the script reads reduceMotion.matches on each event, so it responds without a reload.

Design and usability decisions

  • Angle range: 8–15 degrees is a reasonable starting range for a text-heavy card; larger angles make small text hard to read mid-tilt. Treat this as a design starting point, not a standard.
  • Perspective distance: try 600px for a dramatic look and 1200px for a restrained one.
  • Touch devices: the script ignores touch pointers so the card never interferes with scrolling. Tilt is decoration; nothing essential depends on hover. Device-orientation tilt is possible but requires a permission prompt on some platforms and is outside this tutorial.
  • Keyboard users: the link remains focusable and visible regardless of tilt. Don’t hide any content behind hover.
  • Performance: only transform is animated, which browsers can usually handle without relayout. Use will-change sparingly; a handful of cards is fine, dozens may not be.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Optional: a moving glare

Reuse the same pointer position for a highlight. In the JS, also set --gx and --gy as percentages ((x + 0.5) * 100 + '%'), then add to .card-bg:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
.card-bg::after {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(circle at var(--gx, 50%) var(--gy, 0%),
    rgb(255 255 255 / 0.25), transparent 55%);
  pointer-events: none;
}

Because the card-bg element itself has no opacity or filter applied, this pseudo-element does not break the 3D hierarchy.

Test before shipping

  • Move the pointer across all four corners and confirm the angle never exceeds your maximum.
  • Leave the scene quickly and confirm the card settles to flat.
  • Toggle your OS “reduce motion” setting and confirm the card is static.
  • Tab to the button and confirm it is visible and operable.
  • Check Chrome, Firefox and Safari on desktop, plus a touch device, since this tutorial’s code has not been verified across a browser matrix.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

Your email address will not be published. Required fields are marked *

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
Windows Errors? Fix Them Before They SpreadFree repair scan
Outdated Drivers Are Slowing You DownFree scan - exact matches

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.