15 CSS 3D Tilt Hover Cards10 / 15

Vanilla JSMIT licensed

Dashboard Analytics KPI Widget Card

A FinTech KPI widget with enterprise-grade restraint: perspective(1600px) with tilt capped at ±4°, a drop-shadow that deepens as the card lifts 6px on hover, and a CSS-only sparkline of flex bars that stagger-grow when the tilt arms. Proof that 3D belongs in dashboards — at homeopathic doses.

Published

Live Demo
Try it

The code

<section class="thc-10" aria-label="Dashboard KPI widget tilt card demo">
  <article class="thc-10__card" tabindex="0" aria-label="Monthly recurring revenue: 84,200 dollars, up 12.4 percent">
    <div class="thc-10__head">
      <p class="thc-10__label">Monthly Recurring Revenue</p>
      <span class="thc-10__delta">&#9650; 12.4%</span>
    </div>
    <p class="thc-10__value">$84,200</p>
    <div class="thc-10__spark" aria-hidden="true">
      <i style="--v:32"></i><i style="--v:41"></i><i style="--v:38"></i><i style="--v:52"></i><i style="--v:47"></i><i style="--v:58"></i><i style="--v:54"></i><i style="--v:66"></i><i style="--v:61"></i><i style="--v:72"></i><i style="--v:69"></i><i style="--v:83"></i><i style="--v:92"></i>
    </div>
    <p class="thc-10__foot">vs. $74,900 last month</p>
  </article>
</section>
.thc-10,
.thc-10 *,
.thc-10 *::before,
.thc-10 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.thc-10 {
  --acc: oklch(0.7 0.15 150);
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 48px 24px;
  background: linear-gradient(180deg,oklch(0.975 0.005 250),oklch(0.94 0.01 250));
}

.thc-10 .thc-10__card {
  width: min(300px,100%);
  padding: 24px;
  border-radius: 18px;
  background: #fff;
  border: 1px solid oklch(0.9 0.008 250);
  transform: perspective(1600px) rotateX(var(--rx,0deg)) rotateY(var(--ry,0deg)) translateY(var(--lift,0px));
  transition: transform .5s cubic-bezier(.2,.9,.3,1.05),box-shadow .5s ease;
  box-shadow: 0 10px 24px -18px oklch(0.4 0.03 250 / .5);
  cursor: default;
}

.thc-10 .thc-10__card.is-live {
  --lift: -6px;
  transition: transform .09s linear,box-shadow .4s ease;
  box-shadow: 0 30px 50px -24px oklch(0.4 0.05 250 / .55);
}

.thc-10 .thc-10__card:focus-visible {
  outline: 3px solid var(--acc);
  outline-offset: 4px;
}

.thc-10 .thc-10__head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
}

.thc-10 .thc-10__label {
  font-size: 12px;
  font-weight: 600;
  color: oklch(0.55 0.02 250);
}

.thc-10 .thc-10__delta {
  font-size: 11px;
  font-weight: 700;
  color: var(--acc);
  background: color-mix(in oklab,var(--acc) 14%,white);
  border-radius: 999px;
  padding: 5px 9px;
}

.thc-10 .thc-10__value {
  margin-top: 12px;
  font-size: 34px;
  font-weight: 800;
  letter-spacing: -.02em;
  color: oklch(0.22 0.02 250);
  font-variant-numeric: tabular-nums;
}

.thc-10 .thc-10__spark {
  display: flex;
  align-items: flex-end;
  gap: 4px;
  height: 52px;
  margin-top: 18px;
}

.thc-10 .thc-10__spark i {
  flex: 1;
  height: calc(var(--v) * 1%);
  border-radius: 3px 3px 0 0;
  background: linear-gradient(180deg,var(--acc),color-mix(in oklab,var(--acc) 45%,white));
  transform-origin: bottom;
}

.thc-10 .thc-10__card.is-live .thc-10__spark i {
  animation: thc-10-grow .4s cubic-bezier(.2,.9,.3,1.1) backwards;
}

.thc-10 .thc-10__spark i:nth-child(2n) {
  animation-delay: .05s;
}

.thc-10 .thc-10__spark i:nth-child(3n) {
  animation-delay: .1s;
}

.thc-10 .thc-10__spark i:nth-child(5n) {
  animation-delay: .15s;
}

@keyframes thc-10-grow {
  from {
    transform: scaleY(.55);
  }
}

.thc-10 .thc-10__foot {
  margin-top: 14px;
  font-size: 11.5px;
  color: oklch(0.6 0.015 250);
}

@media (prefers-reduced-motion: reduce) {
  .thc-10 .thc-10__card {
    transition: none;
  }

  .thc-10 .thc-10__card.is-live .thc-10__spark i {
    animation: none;
  }
}
(() => {
  const card = document.querySelector('.thc-10 .thc-10__card');
  if (!card) return;
  if (!matchMedia('(hover:hover) and (pointer:fine)').matches) return;
  if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
  const MAX = 4; // enterprise dose: elevation, not rotation
  card.addEventListener('pointermove', (e) => {
    const r = card.getBoundingClientRect();
    const px = (e.clientX - r.left) / r.width, py = (e.clientY - r.top) / r.height;
    card.style.setProperty('--rx', ((0.5 - py) * MAX).toFixed(2) + 'deg');
    card.style.setProperty('--ry', ((px - 0.5) * MAX).toFixed(2) + 'deg');
    card.classList.add('is-live'); // adds the -6px lift + deep shadow + bar re-grow
  });
  card.addEventListener('pointerleave', () => {
    card.classList.remove('is-live');
    card.style.setProperty('--rx', '0deg');
    card.style.setProperty('--ry', '0deg');
  });
})();
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 3D Tilt Hover 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: Dashboard Analytics KPI Widget Card
Source: https://codefronts.com/components/css-3d-tilt-hover-cards/dashboard-analytics-kpi-widget-card/

A FinTech KPI widget with enterprise-grade restraint: perspective(1600px) with tilt capped at ±4°, a drop-shadow that deepens as the card lifts 6px on hover, and a CSS-only sparkline of flex bars that stagger-grow when the tilt arms. Proof that 3D belongs in dashboards — at homeopathic doses.
## HTML
```html
<section class="thc-10" aria-label="Dashboard KPI widget tilt card demo">
  <article class="thc-10__card" tabindex="0" aria-label="Monthly recurring revenue: 84,200 dollars, up 12.4 percent">
    <div class="thc-10__head">
      <p class="thc-10__label">Monthly Recurring Revenue</p>
      <span class="thc-10__delta">&#9650; 12.4%</span>
    </div>
    <p class="thc-10__value">$84,200</p>
    <div class="thc-10__spark" aria-hidden="true">
      <i style="--v:32"></i><i style="--v:41"></i><i style="--v:38"></i><i style="--v:52"></i><i style="--v:47"></i><i style="--v:58"></i><i style="--v:54"></i><i style="--v:66"></i><i style="--v:61"></i><i style="--v:72"></i><i style="--v:69"></i><i style="--v:83"></i><i style="--v:92"></i>
    </div>
    <p class="thc-10__foot">vs. $74,900 last month</p>
  </article>
</section>
```
## CSS
```css
.thc-10,
.thc-10 *,
.thc-10 *::before,
.thc-10 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.thc-10 {
  --acc: oklch(0.7 0.15 150);
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 48px 24px;
  background: linear-gradient(180deg,oklch(0.975 0.005 250),oklch(0.94 0.01 250));
}

.thc-10 .thc-10__card {
  width: min(300px,100%);
  padding: 24px;
  border-radius: 18px;
  background: #fff;
  border: 1px solid oklch(0.9 0.008 250);
  transform: perspective(1600px) rotateX(var(--rx,0deg)) rotateY(var(--ry,0deg)) translateY(var(--lift,0px));
  transition: transform .5s cubic-bezier(.2,.9,.3,1.05),box-shadow .5s ease;
  box-shadow: 0 10px 24px -18px oklch(0.4 0.03 250 / .5);
  cursor: default;
}

.thc-10 .thc-10__card.is-live {
  --lift: -6px;
  transition: transform .09s linear,box-shadow .4s ease;
  box-shadow: 0 30px 50px -24px oklch(0.4 0.05 250 / .55);
}

.thc-10 .thc-10__card:focus-visible {
  outline: 3px solid var(--acc);
  outline-offset: 4px;
}

.thc-10 .thc-10__head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
}

.thc-10 .thc-10__label {
  font-size: 12px;
  font-weight: 600;
  color: oklch(0.55 0.02 250);
}

.thc-10 .thc-10__delta {
  font-size: 11px;
  font-weight: 700;
  color: var(--acc);
  background: color-mix(in oklab,var(--acc) 14%,white);
  border-radius: 999px;
  padding: 5px 9px;
}

.thc-10 .thc-10__value {
  margin-top: 12px;
  font-size: 34px;
  font-weight: 800;
  letter-spacing: -.02em;
  color: oklch(0.22 0.02 250);
  font-variant-numeric: tabular-nums;
}

.thc-10 .thc-10__spark {
  display: flex;
  align-items: flex-end;
  gap: 4px;
  height: 52px;
  margin-top: 18px;
}

.thc-10 .thc-10__spark i {
  flex: 1;
  height: calc(var(--v) * 1%);
  border-radius: 3px 3px 0 0;
  background: linear-gradient(180deg,var(--acc),color-mix(in oklab,var(--acc) 45%,white));
  transform-origin: bottom;
}

.thc-10 .thc-10__card.is-live .thc-10__spark i {
  animation: thc-10-grow .4s cubic-bezier(.2,.9,.3,1.1) backwards;
}

.thc-10 .thc-10__spark i:nth-child(2n) {
  animation-delay: .05s;
}

.thc-10 .thc-10__spark i:nth-child(3n) {
  animation-delay: .1s;
}

.thc-10 .thc-10__spark i:nth-child(5n) {
  animation-delay: .15s;
}

@keyframes thc-10-grow {
  from {
    transform: scaleY(.55);
  }
}

.thc-10 .thc-10__foot {
  margin-top: 14px;
  font-size: 11.5px;
  color: oklch(0.6 0.015 250);
}

@media (prefers-reduced-motion: reduce) {
  .thc-10 .thc-10__card {
    transition: none;
  }

  .thc-10 .thc-10__card.is-live .thc-10__spark i {
    animation: none;
  }
}
```

## JavaScript
```js
(() => {
  const card = document.querySelector('.thc-10 .thc-10__card');
  if (!card) return;
  if (!matchMedia('(hover:hover) and (pointer:fine)').matches) return;
  if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
  const MAX = 4; // enterprise dose: elevation, not rotation
  card.addEventListener('pointermove', (e) => {
    const r = card.getBoundingClientRect();
    const px = (e.clientX - r.left) / r.width, py = (e.clientY - r.top) / r.height;
    card.style.setProperty('--rx', ((0.5 - py) * MAX).toFixed(2) + 'deg');
    card.style.setProperty('--ry', ((px - 0.5) * MAX).toFixed(2) + 'deg');
    card.classList.add('is-live'); // adds the -6px lift + deep shadow + bar re-grow
  });
  card.addEventListener('pointerleave', () => {
    card.classList.remove('is-live');
    card.style.setProperty('--rx', '0deg');
    card.style.setProperty('--ry', '0deg');
  });
})();
```

How this works

Dashboards need the longest lens in the collection: perspective(1600px) flattens the projection so a 4° tilt reads as elevation, not rotation — the card feels picked up, not flipped. Hover adds translateY(-6px) in the same transform and swaps a resting shadow for a deeper one; because both shadows are defined on the same element and transition together, the lift reads as one physical motion.

The sparkline is thirteen flex-column divs whose heights come from inline --v custom properties (height:calc(var(--v) * 1%)) — no SVG, no canvas, and it inherits currentColor theming. On is-live the bars re-run a 400ms stagger-grow keyframe, a micro-reward for attention that costs nothing when idle.

Make it yours

  • Feed real data by writing --v per bar server-side or from JS — the CSS never changes.
  • Re-metric the accent: positive-trend green (150) → risk red (25) by one hue swap.
  • Drop the bar re-grow animation for always-on dashboards where cards re-render often.
  • Wrap the widget in a <button> if clicking opens a drill-down — the styles are element-agnostic.

Gotchas — read before shipping

  • Long perspective + small angle is the whole aesthetic — 800px/12° turns a KPI card into a game card.
  • Transition transform and box-shadow with the SAME duration or the lift visually detaches from its shadow.
  • Don't animate bar heights on a timer; motion in peripheral vision makes numeric dashboards feel unstable.

Browser support

ChromeSafariFirefoxEdge
111+16.4+113+111+

calc()-driven bar heights and oklch tints; every layout feature is 2020-era or older.

Techniques used in this demo

Search CodeFronts

Loading…