20 CSS Profile Cards08 / 20

CSS + JSMIT licensed

3D Perspective Tilt Profile Card

A card that tilts in 3D toward the cursor with a moving specular sheen, driven by a small pointer handler that batches updates through requestAnimationFrame.

Published

Live Demo
Try it

The code

<div class="pc-08">
  <div class="pc-08__card" id="pc-08-tilt">
    <div class="pc-08__shine"></div>
    <div class="pc-08__content">
      <div class="pc-08__pfp">TN</div>
      <div class="pc-08__info">
        <h3>Theo Nakamura</h3>
        <p>3D / WebGL Developer</p>
      </div>
      <div class="pc-08__foot">
        <span class="pc-08__chip">Three.js</span>
        <span class="pc-08__chip">GLSL</span>
        <span class="pc-08__chip">Blender</span>
      </div>
    </div>
  </div>
</div>
.pc-08,
.pc-08 *,
.pc-08 *::before,
.pc-08 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.pc-08 ::selection {
  background: #ff4d6d;
  color: #fff;
}

.pc-08 {
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 48px 20px;
  background: #0a0710;
  perspective: 900px;
  color: #fff;
}

.pc-08__card {
  position: relative;
  width: 320px;
  border-radius: 24px;
  padding: 30px;
  overflow: hidden;
  background: linear-gradient(150deg,#241033,#3a0f2e);
  border: 1px solid rgba(255,255,255,.1);
  box-shadow: 0 30px 70px -28px rgba(0,0,0,.85);
  transform-style: preserve-3d;
  transition: transform .12s ease-out;
  will-change: transform;
}

.pc-08__shine {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(300px 300px at var(--mx,50%) var(--my,0%),rgba(255,255,255,.22),transparent 60%);
  transition: background .1s;
}

.pc-08__content {
  transform: translateZ(40px);
}

.pc-08__pfp {
  width: 84px;
  height: 84px;
  border-radius: 22px;
  display: grid;
  place-items: center;
  font-weight: 800;
  font-size: 1.6rem;
  background: linear-gradient(145deg,#ff4d6d,#c44dff);
  box-shadow: 0 14px 30px -10px #ff4d6d;
}

.pc-08__info {
  margin-top: 20px;
}

.pc-08__info h3 {
  font-size: 1.35rem;
  font-weight: 800;
}

.pc-08__info p {
  color: rgba(255,255,255,.6);
  font-size: .86rem;
  margin-top: 3px;
}

.pc-08__foot {
  display: flex;
  gap: 8px;
  margin-top: 22px;
}

.pc-08__chip {
  font-size: .72rem;
  padding: 6px 11px;
  border-radius: 9px;
  background: rgba(255,255,255,.1);
  border: 1px solid rgba(255,255,255,.14);
}

.pc-08__hint {
  position: absolute;
  bottom: 12px;
  right: 16px;
  font-size: .66rem;
  color: rgba(255,255,255,.3);
  letter-spacing: .1em;
}

@media (prefers-reduced-motion: reduce) {
  .pc-08__card {
    transition: none;
  }
}
(function(){
  var card = document.getElementById('pc-08-tilt');
  if(!card) return;
  var raf = null, tx=0, ty=0, mx=50, my=0;
  function onMove(e){
    var r = card.getBoundingClientRect();
    var px = (e.clientX - r.left) / r.width;
    var py = (e.clientY - r.top) / r.height;
    tx = (py - .5) * -16;
    ty = (px - .5) * 16;
    mx = px * 100; my = py * 100;
    if(!raf) raf = requestAnimationFrame(apply);
  }
  function apply(){
    raf = null;
    card.style.transform = 'rotateX(' + tx + 'deg) rotateY(' + ty + 'deg)';
    card.style.setProperty('--mx', mx + '%');
    card.style.setProperty('--my', my + '%');
  }
  function reset(){
    if(raf) cancelAnimationFrame(raf), raf=null;
    card.style.transform = 'rotateX(0) rotateY(0)';
    card.style.setProperty('--mx','50%');
    card.style.setProperty('--my','0%');
  }
  card.addEventListener('pointermove', onMove);
  card.addEventListener('pointerleave', reset);
})();
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Profile Card from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: 3D Perspective Tilt Profile Card
Source: https://codefronts.com/components/css-profile-cards/3d-perspective-tilt-profile-card/

A card that tilts in 3D toward the cursor with a moving specular sheen, driven by a small pointer handler that batches updates through requestAnimationFrame.
## HTML
```html
<div class="pc-08">
  <div class="pc-08__card" id="pc-08-tilt">
    <div class="pc-08__shine"></div>
    <div class="pc-08__content">
      <div class="pc-08__pfp">TN</div>
      <div class="pc-08__info">
        <h3>Theo Nakamura</h3>
        <p>3D / WebGL Developer</p>
      </div>
      <div class="pc-08__foot">
        <span class="pc-08__chip">Three.js</span>
        <span class="pc-08__chip">GLSL</span>
        <span class="pc-08__chip">Blender</span>
      </div>
    </div>
  </div>
</div>
```
## CSS
```css
.pc-08,
.pc-08 *,
.pc-08 *::before,
.pc-08 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.pc-08 ::selection {
  background: #ff4d6d;
  color: #fff;
}

.pc-08 {
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 48px 20px;
  background: #0a0710;
  perspective: 900px;
  color: #fff;
}

.pc-08__card {
  position: relative;
  width: 320px;
  border-radius: 24px;
  padding: 30px;
  overflow: hidden;
  background: linear-gradient(150deg,#241033,#3a0f2e);
  border: 1px solid rgba(255,255,255,.1);
  box-shadow: 0 30px 70px -28px rgba(0,0,0,.85);
  transform-style: preserve-3d;
  transition: transform .12s ease-out;
  will-change: transform;
}

.pc-08__shine {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(300px 300px at var(--mx,50%) var(--my,0%),rgba(255,255,255,.22),transparent 60%);
  transition: background .1s;
}

.pc-08__content {
  transform: translateZ(40px);
}

.pc-08__pfp {
  width: 84px;
  height: 84px;
  border-radius: 22px;
  display: grid;
  place-items: center;
  font-weight: 800;
  font-size: 1.6rem;
  background: linear-gradient(145deg,#ff4d6d,#c44dff);
  box-shadow: 0 14px 30px -10px #ff4d6d;
}

.pc-08__info {
  margin-top: 20px;
}

.pc-08__info h3 {
  font-size: 1.35rem;
  font-weight: 800;
}

.pc-08__info p {
  color: rgba(255,255,255,.6);
  font-size: .86rem;
  margin-top: 3px;
}

.pc-08__foot {
  display: flex;
  gap: 8px;
  margin-top: 22px;
}

.pc-08__chip {
  font-size: .72rem;
  padding: 6px 11px;
  border-radius: 9px;
  background: rgba(255,255,255,.1);
  border: 1px solid rgba(255,255,255,.14);
}

.pc-08__hint {
  position: absolute;
  bottom: 12px;
  right: 16px;
  font-size: .66rem;
  color: rgba(255,255,255,.3);
  letter-spacing: .1em;
}

@media (prefers-reduced-motion: reduce) {
  .pc-08__card {
    transition: none;
  }
}
```

## JavaScript
```js
(function(){
  var card = document.getElementById('pc-08-tilt');
  if(!card) return;
  var raf = null, tx=0, ty=0, mx=50, my=0;
  function onMove(e){
    var r = card.getBoundingClientRect();
    var px = (e.clientX - r.left) / r.width;
    var py = (e.clientY - r.top) / r.height;
    tx = (py - .5) * -16;
    ty = (px - .5) * 16;
    mx = px * 100; my = py * 100;
    if(!raf) raf = requestAnimationFrame(apply);
  }
  function apply(){
    raf = null;
    card.style.transform = 'rotateX(' + tx + 'deg) rotateY(' + ty + 'deg)';
    card.style.setProperty('--mx', mx + '%');
    card.style.setProperty('--my', my + '%');
  }
  function reset(){
    if(raf) cancelAnimationFrame(raf), raf=null;
    card.style.transform = 'rotateX(0) rotateY(0)';
    card.style.setProperty('--mx','50%');
    card.style.setProperty('--my','0%');
  }
  card.addEventListener('pointermove', onMove);
  card.addEventListener('pointerleave', reset);
})();
```

How this works

The card sits in a perspective context; a pointer handler maps the cursor's position within the card to rotateX/rotateY values and writes them to the element's transform. Updates are coalesced inside requestAnimationFrame so the DOM is touched at most once per frame, keeping input latency (INP) low.

The same handler updates two custom properties feeding a radial --mx/--my highlight, so a specular sheen tracks the pointer for a glossy, tactile feel. On pointerleave the transform resets to flat; with JS disabled the card simply renders static and fully readable.

Make it yours

  • Change tilt intensity by scaling the (py-.5) * -16 multipliers in the handler.
  • Soften or sharpen the settle with the transition:transform .12s on the card.
  • Recolour or resize the sheen via the radial-gradient on .pc-08__shine.
  • Increase the parallax pop by raising the translateZ(40px) on the content layer.

Gotchas — read before shipping

  • Read layout with getBoundingClientRect() once per move and never inside the rAF write step, to avoid layout thrash.
  • pointermove fires rapidly — always batch through requestAnimationFrame rather than styling on every event.
  • Provide a flat static state for no-JS and reduced-motion users; the tilt is an enhancement, not the content.
  • Set will-change:transform (already applied) sparingly — over-promoting layers hurts memory on mobile.

Browser support

ChromeSafariFirefoxEdge
55+13+59+55+

Pointer Events plus 3D transforms; degrades to a static card without JS.

Techniques used in this demo

3D transforms (perspective + preserve-3d)radial-gradient()MDN ↗place-items / place-contentMDN ↗

Search CodeFronts

Loading…