Live preview needs JavaScript. The code below works without any dependencies.
Prompt for your AI assistant
Paste this into Claude, Cursor or ChatGPT. The settings update as you move the controls above.
Build "Skew hover cards" for my site using the Enrichr Library element at https://enrichr.dev/elements/skew-hover-cards.html.
Read the machine-readable spec and source at https://enrichr.dev/elements/skew-hover-cards.json (markup, CSS, JavaScript, parameters).
Requirements: vanilla JavaScript and CSS, no new dependencies; keep the behaviour of the source unchanged; respect prefers-reduced-motion; work on touch screens; keep it accessible.
Settings:
- strength: 1
- settle: 0.8
- background: #0c1e18
- card: #173b2e
- accent: #e6ff55
- text: #fbf8f1
Verify: reduced motion shows no movement, it works at 375px wide, there are no console errors, and scrolling stays smooth.The same spec as JSON, for tools that read it directly: /elements/skew-hover-cards.json.
Code
Mount it on an element that contains the markup. It returns an object with update(options) and destroy().
HTML
<div class="ef-shc">
<div class="ef-shc__grid">
<div class="ef-shc__card">
<span class="ef-shc__pill"><span class="ef-shc__glyph" aria-hidden="true">✦</span> Skew</span>
<span class="ef-shc__label">hover · tilt</span>
<svg class="ef-shc__mark" viewBox="0 0 50 50" aria-hidden="true" focusable="false"><path fill="currentColor" d="m34.213 25.361 c0 2.655 1.381 4.808 3.086 4.808 0.781 0 1.494 -0.452 2.038 -1.198 0.267 -0.366 0.688 -0.589 1.142 -0.589 h0.054 c0.774 0 1.401 0.627 1.401 1.401 v10.65 h-10.651 c-0.774 0 -1.401 0.627 -1.401 1.401 v0.054 c0 0.453 0.222 0.874 0.589 1.141 0.746 0.544 1.198 1.257 1.198 2.038 0 1.705 -2.152 3.086 -4.808 3.086 -2.655 0 -4.808 -1.381 -4.808 -3.086 0 -0.781 0.452 -1.494 1.198 -2.038 0.366 -0.267 0.589 -0.688 0.589 -1.142 v-0.054 c0 -0.774 -0.627 -1.401 -1.401 -1.401 h-10.65 v-10.651 c0 -0.774 -0.627 -1.401 -1.401 -1.401 h-0.054 c-0.453 0 -0.875 0.222 -1.142 0.589 -0.543 0.746 -1.257 1.198 -2.038 1.198 -1.705 0 -3.086 -2.152 -3.086 -4.808 0 -2.655 1.381 -4.808 3.086 -4.808 0.781 0 1.495 0.452 2.038 1.198 0.267 0.366 0.688 0.589 1.141 0.589 h0.054 c0.774 0 1.401 -0.627 1.401 -1.401 v-10.65 h10.65 c0.774 0 1.401 -0.627 1.401 -1.401 v-0.054 c0 -0.453 -0.222 -0.875 -0.589 -1.142 -0.746 -0.543 -1.198 -1.257 -1.198 -2.038 0 -1.705 2.152 -3.086 4.808 -3.086 2.655 0 4.808 1.381 4.808 3.086 0 0.781 -0.452 1.495 -1.198 2.038 -0.366 0.267 -0.589 0.688 -0.589 1.141 v0.054 c0 0.774 0.627 1.401 1.401 1.401 h10.65 v10.65 c0 0.774 -0.627 1.401 -1.401 1.401 h-0.053 c-0.453 0 -0.874 -0.222 -1.141 -0.589 -0.544 -0.746 -1.257 -1.198 -2.038 -1.198 -1.705 0 -3.086 2.152 -3.086 4.808 z"/></svg>
</div>
<div class="ef-shc__card">
<span class="ef-shc__pill"><span class="ef-shc__glyph" aria-hidden="true">◈</span> Mouse</span>
<span class="ef-shc__label">hover · twist</span>
<svg class="ef-shc__mark" viewBox="0 0 50 50" aria-hidden="true" focusable="false"><path fill="currentColor" d="m34.213 25.361 c0 2.655 1.381 4.808 3.086 4.808 0.781 0 1.494 -0.452 2.038 -1.198 0.267 -0.366 0.688 -0.589 1.142 -0.589 h0.054 c0.774 0 1.401 0.627 1.401 1.401 v10.65 h-10.651 c-0.774 0 -1.401 0.627 -1.401 1.401 v0.054 c0 0.453 0.222 0.874 0.589 1.141 0.746 0.544 1.198 1.257 1.198 2.038 0 1.705 -2.152 3.086 -4.808 3.086 -2.655 0 -4.808 -1.381 -4.808 -3.086 0 -0.781 0.452 -1.494 1.198 -2.038 0.366 -0.267 0.589 -0.688 0.589 -1.142 v-0.054 c0 -0.774 -0.627 -1.401 -1.401 -1.401 h-10.65 v-10.651 c0 -0.774 -0.627 -1.401 -1.401 -1.401 h-0.054 c-0.453 0 -0.875 0.222 -1.142 0.589 -0.543 0.746 -1.257 1.198 -2.038 1.198 -1.705 0 -3.086 -2.152 -3.086 -4.808 0 -2.655 1.381 -4.808 3.086 -4.808 0.781 0 1.495 0.452 2.038 1.198 0.267 0.366 0.688 0.589 1.141 0.589 h0.054 c0.774 0 1.401 -0.627 1.401 -1.401 v-10.65 h10.65 c0.774 0 1.401 -0.627 1.401 -1.401 v-0.054 c0 -0.453 -0.222 -0.875 -0.589 -1.142 -0.746 -0.543 -1.198 -1.257 -1.198 -2.038 0 -1.705 2.152 -3.086 4.808 -3.086 2.655 0 4.808 1.381 4.808 3.086 0 0.781 -0.452 1.495 -1.198 2.038 -0.366 0.267 -0.589 0.688 -0.589 1.141 v0.054 c0 0.774 0.627 1.401 1.401 1.401 h10.65 v10.65 c0 0.774 -0.627 1.401 -1.401 1.401 h-0.053 c-0.453 0 -0.874 -0.222 -1.141 -0.589 -0.544 -0.746 -1.257 -1.198 -2.038 -1.198 -1.705 0 -3.086 2.152 -3.086 4.808 z"/></svg>
</div>
<div class="ef-shc__card">
<span class="ef-shc__pill"><span class="ef-shc__glyph" aria-hidden="true">◆</span> 3D</span>
<span class="ef-shc__label">hover · rotate</span>
<svg class="ef-shc__mark" viewBox="0 0 50 50" aria-hidden="true" focusable="false"><path fill="currentColor" d="m34.213 25.361 c0 2.655 1.381 4.808 3.086 4.808 0.781 0 1.494 -0.452 2.038 -1.198 0.267 -0.366 0.688 -0.589 1.142 -0.589 h0.054 c0.774 0 1.401 0.627 1.401 1.401 v10.65 h-10.651 c-0.774 0 -1.401 0.627 -1.401 1.401 v0.054 c0 0.453 0.222 0.874 0.589 1.141 0.746 0.544 1.198 1.257 1.198 2.038 0 1.705 -2.152 3.086 -4.808 3.086 -2.655 0 -4.808 -1.381 -4.808 -3.086 0 -0.781 0.452 -1.494 1.198 -2.038 0.366 -0.267 0.589 -0.688 0.589 -1.142 v-0.054 c0 -0.774 -0.627 -1.401 -1.401 -1.401 h-10.65 v-10.651 c0 -0.774 -0.627 -1.401 -1.401 -1.401 h-0.054 c-0.453 0 -0.875 0.222 -1.142 0.589 -0.543 0.746 -1.257 1.198 -2.038 1.198 -1.705 0 -3.086 -2.152 -3.086 -4.808 0 -2.655 1.381 -4.808 3.086 -4.808 0.781 0 1.495 0.452 2.038 1.198 0.267 0.366 0.688 0.589 1.141 0.589 h0.054 c0.774 0 1.401 -0.627 1.401 -1.401 v-10.65 h10.65 c0.774 0 1.401 -0.627 1.401 -1.401 v-0.054 c0 -0.453 -0.222 -0.875 -0.589 -1.142 -0.746 -0.543 -1.198 -1.257 -1.198 -2.038 0 -1.705 2.152 -3.086 4.808 -3.086 2.655 0 4.808 1.381 4.808 3.086 0 0.781 -0.452 1.495 -1.198 2.038 -0.366 0.267 -0.589 0.688 -0.589 1.141 v0.054 c0 0.774 0.627 1.401 1.401 1.401 h10.65 v10.65 c0 0.774 -0.627 1.401 -1.401 1.401 h-0.053 c-0.453 0 -0.874 -0.222 -1.141 -0.589 -0.544 -0.746 -1.257 -1.198 -2.038 -1.198 -1.705 0 -3.086 2.152 -3.086 4.808 z"/></svg>
</div>
</div>
<p class="ef-shc__info"><span class="ef-shc__dot" aria-hidden="true">⬤</span>hover each card · independent 3D skew<span class="ef-shc__dot" aria-hidden="true">⬤</span></p>
</div>CSS
/* Enrichr Library · https://enrichr.dev */
.ef-shc { --ef-shc-bg: #0c1e18; --ef-shc-card: #173b2e; --ef-shc-acc: #e6ff55; --ef-shc-ink: #fbf8f1; position: relative; box-sizing: border-box; width: 100%; height: 100%; min-height: 30rem; flex: 1 1 auto; display: flex; align-items: center; justify-content: center; padding: 1.5rem; overflow: hidden; border-radius: 1rem; background: linear-gradient(145deg, color-mix(in srgb, var(--ef-shc-bg) 58%, #000), var(--ef-shc-bg)); font-family: "Parkinsans", "Space Grotesk", system-ui, sans-serif; }
.ef-shc .ef-shc__grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 2rem; width: 100%; max-width: 780px; margin: 0 auto; perspective: 1000px; }
.ef-shc .ef-shc__card { position: relative; box-sizing: border-box; aspect-ratio: 3 / 4; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 1.2rem; border: 1px solid color-mix(in srgb, var(--ef-shc-ink) 4%, transparent); border-radius: 2.4rem; background: linear-gradient(160deg, var(--ef-shc-card), color-mix(in srgb, var(--ef-shc-card) 58%, #000)); box-shadow: 0 20px 40px rgba(0, 0, 0, 0.7), inset 0 1px 2px color-mix(in srgb, var(--ef-shc-ink) 4%, transparent); color: var(--ef-shc-ink); font-size: 1.4rem; font-weight: 600; letter-spacing: 0.03em; text-shadow: 0 4px 10px rgba(0, 0, 0, 0.6); -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px); cursor: default; transform-style: preserve-3d; transform-origin: 50% 50%; will-change: transform; transition: box-shadow 0.25s ease; touch-action: pan-y; }
.ef-shc .ef-shc__card:hover { border-color: color-mix(in srgb, var(--ef-shc-ink) 8%, transparent); box-shadow: 0 25px 50px rgba(0, 0, 0, 0.8), 0 0 20px color-mix(in srgb, var(--ef-shc-acc) 8%, transparent); }
.ef-shc .ef-shc__pill { padding: 0.4rem 1.4rem; border: 1px solid color-mix(in srgb, var(--ef-shc-ink) 5%, transparent); border-radius: 60px; background: color-mix(in srgb, var(--ef-shc-ink) 3%, transparent); -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); font-size: 1.2rem; line-height: 1.5; letter-spacing: 0.08em; text-transform: uppercase; white-space: nowrap; pointer-events: none; }
.ef-shc .ef-shc__label { margin-top: 0.8rem; text-align: center; font-size: 0.7rem; font-weight: 400; letter-spacing: 0.12em; text-transform: uppercase; opacity: 0.35; pointer-events: none; }
.ef-shc .ef-shc__mark { position: absolute; right: 1.8rem; bottom: 1.6rem; width: 2rem; height: 2rem; color: var(--ef-shc-acc); opacity: 0.14; pointer-events: none; transition: opacity 0.3s; }
.ef-shc .ef-shc__info { position: absolute; bottom: 2rem; left: 50%; margin: 0; padding: 0.5rem 2rem; border: 1px solid color-mix(in srgb, var(--ef-shc-ink) 4%, transparent); border-radius: 60px; background: color-mix(in srgb, color-mix(in srgb, var(--ef-shc-bg) 45%, #000) 80%, transparent); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6); color: color-mix(in srgb, var(--ef-shc-ink) 70%, var(--ef-shc-bg)); font-family: "Instrument Sans", system-ui, sans-serif; font-size: 0.85rem; line-height: 1.5; letter-spacing: 0.02em; white-space: nowrap; opacity: 1; transform: translateX(-50%); pointer-events: none; }
.ef-shc .ef-shc__dot { display: inline-block; margin: 0 0.4rem; color: var(--ef-shc-acc); font-style: normal; }
.ef-shc.is-narrow .ef-shc__grid { gap: 1rem; max-width: 380px; }
.ef-shc.is-narrow .ef-shc__card { border-radius: 1.8rem; font-size: 1.1rem; }
.ef-shc.is-narrow .ef-shc__pill { padding: 0.2rem 1rem; font-size: 0.9rem; }
.ef-shc.is-narrow .ef-shc__mark { right: 1.4rem; bottom: 1.2rem; width: 1.4rem; height: 1.4rem; }
.ef-shc.is-narrow .ef-shc__info { padding: 0.3rem 1.2rem; font-size: 0.7rem; white-space: normal; text-align: center; width: max-content; max-width: calc(100% - 2rem); box-sizing: border-box; }
.ef-shc.is-tiny .ef-shc__pill { padding: 0.15rem 0.6rem; font-size: 0.72rem; letter-spacing: 0.04em; }
.ef-shc.is-tiny .ef-shc__card { padding: 0.5rem; }
.ef-shc.is-tiny .ef-shc__label { font-size: 0.55rem; letter-spacing: 0.04em; white-space: nowrap; }
.ef-shc.is-tiny .ef-shc__mark { right: 0.9rem; bottom: 0.8rem; width: 1rem; height: 1rem; }JavaScript
// Enrichr Library · https://enrichr.dev
// Skew hover cards: three cards that bend in 3D only while the pointer is over them. Each frame, the pointer's place
// inside the hovered card (-0.5 to 0.5 across and down) is mapped straight to a skew (up to 16 and 10 degrees at the
// edges), a twist, a tilt around both axes and a slight push in, all behind a 900 px perspective inside a
// 1000 px perspective row. Each card has its own personality: a stronger response and a small fixed lean per position.
// There is no easing on the way in, the card follows the pointer directly; leaving lets it settle back to flat over
// 0.8 s on a power3 out curve (0.7 s for every card when the window loses focus). Touch works as a short tap-and-settle.
// Nothing runs until a card is hovered. Reduced motion: the cards stay flat and only the hover glow changes.
// mount(root, { strength (1 is the pen), settle (seconds to fall back), background, card, accent, text })
function mount(root, o) {
var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
var box = root.querySelector(".ef-shc");
var cards = [].slice.call(root.querySelectorAll(".ef-shc__card"));
var ZERO = { skx: 0, sky: 0, rz: 0, rx: 0, ry: 0, s: 1 };
var st = cards.map(function () { return { hov: false, mx: 0, my: 0, v: Object.assign({}, ZERO), tw: null }; });
var opts = {}, raf = 0, last = 0, ro = null, offs = [], destroyed = false;
function num(v, d) { v = Number(v); return isFinite(v) ? v : d; }
function clamp(v) { return v < -0.5 ? -0.5 : v > 0.5 ? 0.5 : v; }
function p3o(p) { return 1 - Math.pow(1 - p, 4); } // GSAP power3.out
// the pen's mapping from pointer offset to pose, card by card
function pose(i) {
var ox = st[i].mx, oy = st[i].my;
var k = (1 + 0.2 * (i % 3)) * opts.strength;
var dist = Math.abs(ox) + Math.abs(oy);
var fx = 0.08 * (i - 1), fy = 0.06 * (i * 1.2);
return {
skx: ox * 32 * k + fx * 2,
sky: oy * 20 * k + fy * 2,
rz: (ox * 14 + oy * 8) * k + i * 0.5,
rx: oy * 22 * k + fy * 3,
ry: ox * 26 * k + fx * 3,
s: 1 + dist * 0.12 * k * 0.6 + 0.01 * (i - 1)
};
}
function paint(i) {
var v = st[i].v;
cards[i].style.transform = "perspective(900px) rotate(" + v.rz.toFixed(3) + "deg) rotateY(" + v.ry.toFixed(3) + "deg) rotateX(" + v.rx.toFixed(3) +
"deg) skew(" + v.skx.toFixed(3) + "deg," + v.sky.toFixed(3) + "deg) scale(" + v.s.toFixed(4) + ")";
}
function settle(i, dur) {
var s = st[i];
s.hov = false;
s.tw = { from: Object.assign({}, s.v), t: 0, d: dur };
kick();
}
function loop(now) {
raf = 0;
var dt = Math.min(0.1, (now - last) / 1000 || 0); last = now;
var busy = false;
for (var i = 0; i < cards.length; i++) {
var s = st[i];
if (s.hov) { s.tw = null; s.v = pose(i); paint(i); busy = true; }
else if (s.tw) {
s.tw.t += dt;
var p = Math.min(1, s.tw.t / s.tw.d), e = p3o(p);
for (var key in ZERO) s.v[key] = s.tw.from[key] + (ZERO[key] - s.tw.from[key]) * e;
paint(i);
if (p >= 1) { s.tw = null; cards[i].style.transform = ""; } else busy = true;
}
}
if (busy && !document.hidden) raf = requestAnimationFrame(loop);
}
function kick() { if (!raf && !destroyed && !reduce) { last = performance.now(); raf = requestAnimationFrame(loop); } }
function offset(card, e) {
var r = card.getBoundingClientRect();
return [(e.clientX - r.left) / r.width - 0.5, (e.clientY - r.top) / r.height - 0.5];
}
if (!reduce) {
cards.forEach(function (card, i) {
var s = st[i];
function enter(e) { var p = offset(card, e); s.hov = true; s.mx = p[0]; s.my = p[1]; kick(); }
function move(e) { if (!s.hov) return; var p = offset(card, e); s.mx = clamp(p[0]); s.my = clamp(p[1]); kick(); }
function leave() { if (s.hov) settle(i, opts.settle); }
card.addEventListener("pointerenter", enter);
card.addEventListener("pointermove", move);
card.addEventListener("pointerleave", leave);
card.addEventListener("pointercancel", leave);
offs.push(function () {
card.removeEventListener("pointerenter", enter); card.removeEventListener("pointermove", move);
card.removeEventListener("pointerleave", leave); card.removeEventListener("pointercancel", leave);
});
});
var onBlur = function () { cards.forEach(function (c, i) { if (st[i].hov || st[i].tw || cards[i].style.transform) settle(i, opts.settle * 0.875); }); };
window.addEventListener("blur", onBlur);
offs.push(function () { window.removeEventListener("blur", onBlur); });
}
function size() {
var W = box.clientWidth;
box.classList.toggle("is-narrow", W < 600);
box.classList.toggle("is-tiny", W < 420);
}
function update(partial) {
opts = Object.assign({ strength: 1, settle: 0.8, background: "#0c1e18", card: "#173b2e", accent: "#e6ff55", text: "#fbf8f1" }, opts, partial || {});
opts.strength = Math.max(0, num(opts.strength, 1)); opts.settle = Math.max(0.05, num(opts.settle, 0.8));
box.style.setProperty("--ef-shc-bg", opts.background);
box.style.setProperty("--ef-shc-card", opts.card);
box.style.setProperty("--ef-shc-acc", opts.accent);
box.style.setProperty("--ef-shc-ink", opts.text);
if (destroyed) return;
size();
}
update(o);
if ("ResizeObserver" in window) { ro = new ResizeObserver(function () { if (!destroyed) size(); }); ro.observe(box); }
return {
update: update,
destroy: function () {
destroyed = true; cancelAnimationFrame(raf); raf = 0;
offs.forEach(function (f) { f(); }); offs = [];
if (ro) ro.disconnect();
cards.forEach(function (c) { c.style.transform = ""; });
}
};
}
// Usage
var fx = mount(document.querySelector("#my-root"), {"strength":1,"settle":0.8,"background":"#0c1e18","card":"#173b2e","accent":"#e6ff55","text":"#fbf8f1"});How it works
While the pointer is over a card, every frame turns its place inside the card (from minus a half to a half across and down) straight into a pose: a skew of up to 16 and 10 degrees, a twist, a tilt around both axes and a slight push in, behind a 900 px perspective inside a 1000 px perspective row. Each card responds a little more strongly than the one before and has a small fixed lean of its own. There is no easing on the way in, so the card follows the pointer exactly; on leaving it settles back to flat over 0.8 seconds on a power3 out curve, and all cards settle if the window loses focus. The pointer position is read from the card's current on-screen box, as in the original.
Accessibility and performance
The cards are plain text, not controls, so they take no focus and the words read normally. The decorative glyphs and the puzzle mark are hidden from assistive technology. With prefers-reduced-motion the cards stay flat and only the hover glow changes.
Nothing runs until a card is hovered; then one requestAnimationFrame loop writes a single transform per moving card and stops once every card is flat. The cards use will-change: transform so the bend is composited.
| Section | Band |
|---|---|
| Techniques | Pointer interaction, CSS 3D |
| Stack | Vanilla JS, CSS |
| Works with | React, Next.js, Vanilla |
| Dependencies | None |
| Status | tested |
| On CodePen | Open the live pen on our CodePen account; fork it to experiment. |
| Inspired by | Hover-only 3D skew cards by Enrichr on CodePen. Original code, written from scratch. |
Find this technique on a real site
Want to see this technique in the wild? Check any site with the free tools, then build your own version from this page.
Check which animation library a site uses · Extract a site's design tokens · Browse site breakdowns
Licence and provenance
Free to use in personal and client projects during the preview. Final licence terms will be published here before any paid tier starts; ask before reselling the code as a collection.
This is original code. The technique is a common one that we studied by measuring award-winning sites with Enrichr; no third-party code, shaders, fonts or images are used.
More elements
FAQ
Does it work on touch screens?
A tap gives a short bend and settle. Dragging up or down on a card still scrolls the page.
Can I put links or buttons on the cards?
Yes. The pointer maths only reads positions, so clicks pass through. Keep text short, because the skew distorts long lines.
How do I make it calmer?
Lower Strength. At 0.5 the cards lean gently; at 0 only the small fixed lean of each card remains.