01Card holographic mouse tilt
Hovering the card tracks the cursor with a rainbow highlight sweeping across the surface while the whole card tilts toward it.
Best forLimited-edition product cards, merch detail pages that want that shiny trading-card look
mix-blend-mode: color-dodgepointermove → --mx/--myconic-gradient

<main class="stage">
<article class="holo-card is-demo" tabindex="0">
<div class="holo-card__shine" aria-hidden="true"></div>
<div class="holo-card__body">
<span class="holo-card__badge">Limited edition</span>
<h3 class="holo-card__title">Midnight Aurora</h3>
</div>
</article>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-quick; // 카드 기울기·시프트가 전환되는 시간
$easing: $ease-out;
$color: $subject-blue; // 카드 뒷면 그라디언트 색
@include stage($stage-ink);
.holo-card {
--mx: 50%; --my: 35%; --rx: 0deg; --ry: 0deg;
position: relative;
width: min(128px, 40vw);
aspect-ratio: 4 / 5;
border-radius: $r-card;
overflow: hidden;
background: linear-gradient(155deg, $color 0%, $stage-ink 100%);
box-shadow: $shadow-raised;
outline: none;
cursor: pointer;
transform: perspective(700px) rotateX(var(--rx)) rotateY(var(--ry));
transition: transform $duration $easing, box-shadow $duration $easing;
}
.holo-card__shine {
position: absolute;
inset: -20%;
background: conic-gradient(from 0deg, #dcb9d1, #b9d9dc, #c8dcb9, #dcdab9, #dcb9d1);
background-size: 240% 240%;
background-position: var(--mx) var(--my);
mix-blend-mode: color-dodge;
opacity: .55;
pointer-events: none;
transition: opacity $duration $easing, background-position $duration $easing;
}
.holo-card__body {
position: absolute; left: 0; right: 0; bottom: 0;
padding: $sp-4;
background: linear-gradient(0deg, rgba($stage-ink, .78), rgba($stage-ink, 0));
}
.holo-card__badge {
display: inline-block;
padding: $sp-1 $sp-2;
margin-bottom: $sp-2;
border-radius: $r-pill;
background: $stage-paper;
color: $stage-ink;
font-weight: 700;
font-size: 10px;
font-family: inherit;
}
.holo-card__title { margin: 0; color: $stage-paper; font-weight: 700; font-size: clamp(13px, 4vw, 15px); line-height: 1.3; font-family: inherit; }
.holo-card:hover,
.holo-card:focus-visible { box-shadow: $shadow-press; }
.holo-card:hover .holo-card__shine,
.holo-card:focus-visible .holo-card__shine { opacity: .85; }
.holo-card.is-demo { animation: tilt-loop $dur-loop $easing infinite; }
.holo-card.is-demo .holo-card__shine { animation: shine-loop $dur-loop $easing infinite; }
@keyframes tilt-loop {
0%, 10% { transform: perspective(700px) rotateX(0deg) rotateY(0deg); }
35% { transform: perspective(700px) rotateX(-10deg) rotateY(12deg); }
65% { transform: perspective(700px) rotateX(8deg) rotateY(-10deg); }
90%, 100% { transform: perspective(700px) rotateX(0deg) rotateY(0deg); }
}
@keyframes shine-loop {
0%, 10% { background-position: 50% 50%; opacity: .55; }
35% { background-position: 15% 20%; opacity: .9; }
65% { background-position: 85% 80%; opacity: .9; }
90%, 100% { background-position: 50% 50%; opacity: .55; }
}
@media (hover: none) {
.holo-card:active { box-shadow: $shadow-press; }
.holo-card:active .holo-card__shine { opacity: .85; }
}
@media (prefers-reduced-motion: reduce) {
.holo-card { transition: none; }
.holo-card__shine { transition: none; opacity: .7; }
.holo-card.is-demo,
.holo-card.is-demo .holo-card__shine { animation: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.holo-card {
--mx: 50%;
--my: 35%;
--rx: 0deg;
--ry: 0deg;
position: relative;
width: min(128px, 40vw);
aspect-ratio: 4/5;
border-radius: 16px;
overflow: hidden;
background: linear-gradient(155deg, #2f6df6 0%, #17141a 100%);
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
outline: none;
cursor: pointer;
transform: perspective(700px) rotateX(var(--rx)) rotateY(var(--ry));
transition: transform 220ms ease-out, box-shadow 220ms ease-out;
}
.holo-card__shine {
position: absolute;
inset: -20%;
background: conic-gradient(from 0deg, #dcb9d1, #b9d9dc, #c8dcb9, #dcdab9, #dcb9d1);
background-size: 240% 240%;
background-position: var(--mx) var(--my);
mix-blend-mode: color-dodge;
opacity: 0.55;
pointer-events: none;
transition: opacity 220ms ease-out, background-position 220ms ease-out;
}
.holo-card__body {
position: absolute;
left: 0;
right: 0;
bottom: 0;
padding: 16px;
background: linear-gradient(0deg, rgba(23, 20, 26, 0.78), rgba(23, 20, 26, 0));
}
.holo-card__badge {
display: inline-block;
padding: 4px 8px;
margin-bottom: 8px;
border-radius: 999px;
background: #ffffff;
color: #17141a;
font-weight: 700;
font-size: 10px;
font-family: inherit;
}
.holo-card__title {
margin: 0;
color: #ffffff;
font-weight: 700;
font-size: clamp(13px, 4vw, 15px);
line-height: 1.3;
font-family: inherit;
}
.holo-card:hover,
.holo-card:focus-visible {
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
}
.holo-card:hover .holo-card__shine,
.holo-card:focus-visible .holo-card__shine {
opacity: 0.85;
}
.holo-card.is-demo {
animation: tilt-loop 2s ease-out infinite;
}
.holo-card.is-demo .holo-card__shine {
animation: shine-loop 2s ease-out infinite;
}
@keyframes tilt-loop {
0%, 10% {
transform: perspective(700px) rotateX(0deg) rotateY(0deg);
}
35% {
transform: perspective(700px) rotateX(-10deg) rotateY(12deg);
}
65% {
transform: perspective(700px) rotateX(8deg) rotateY(-10deg);
}
90%, 100% {
transform: perspective(700px) rotateX(0deg) rotateY(0deg);
}
}
@keyframes shine-loop {
0%, 10% {
background-position: 50% 50%;
opacity: 0.55;
}
35% {
background-position: 15% 20%;
opacity: 0.9;
}
65% {
background-position: 85% 80%;
opacity: 0.9;
}
90%, 100% {
background-position: 50% 50%;
opacity: 0.55;
}
}
@media (hover: none) {
.holo-card:active {
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
}
.holo-card:active .holo-card__shine {
opacity: 0.85;
}
}
@media (prefers-reduced-motion: reduce) {
.holo-card {
transition: none;
}
.holo-card__shine {
transition: none;
opacity: 0.7;
}
.holo-card.is-demo,
.holo-card.is-demo .holo-card__shine {
animation: none;
}
}var c=document.querySelector('.holo-card');
c.addEventListener('pointermove',function(e){
if(c.classList.contains('is-demo'))return;
var r=c.getBoundingClientRect(),mx=(e.clientX-r.left)/r.width*100,my=(e.clientY-r.top)/r.height*100;
c.style.setProperty('--mx',mx+'%');c.style.setProperty('--my',my+'%');
c.style.setProperty('--rx',((my/100)-.5)*-14+'deg');c.style.setProperty('--ry',((mx/100)-.5)*14+'deg');
});
c.addEventListener('pointerleave',function(){
if(c.classList.contains('is-demo'))return;
c.style.setProperty('--mx','50%');c.style.setProperty('--my','50%');
c.style.setProperty('--rx','0deg');c.style.setProperty('--ry','0deg');
});import { useRef, type CSSProperties, type PointerEvent } from "react";
import styles from "./HoloCardMouseTilt.module.scss";
export type HoloCardMouseTiltProps = {
badge: string;
title: string;
/** 기울기·시프트가 전환되는 시간 (기본 220ms) */
duration?: number;
easing?: string;
/** 카드 뒷면 그라디언트 색 */
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
/** pointermove 로 --mx/--my/--rx/--ry 를 직접 놓는다 — :hover 만으로는 커서 좌표를 못 읽는다 */
export default function HoloCardMouseTilt({
badge,
title,
duration = 220,
easing = "ease-out",
color = "#2f6df6",
}: HoloCardMouseTiltProps) {
const ref = useRef<HTMLDivElement | null>(null);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
const move = (e: PointerEvent<HTMLDivElement>) => {
const el = ref.current;
if (!el) return;
const r = el.getBoundingClientRect();
const mx = ((e.clientX - r.left) / r.width) * 100;
const my = ((e.clientY - r.top) / r.height) * 100;
el.style.setProperty("--mx", `${mx}%`);
el.style.setProperty("--my", `${my}%`);
el.style.setProperty("--rx", `${(my / 100 - 0.5) * -14}deg`);
el.style.setProperty("--ry", `${(mx / 100 - 0.5) * 14}deg`);
};
const leave = () => {
const el = ref.current;
if (!el) return;
el.style.setProperty("--mx", "50%");
el.style.setProperty("--my", "50%");
el.style.setProperty("--rx", "0deg");
el.style.setProperty("--ry", "0deg");
};
return (
<article
ref={ref}
className={styles.card}
style={vars}
tabIndex={0}
onPointerMove={move}
onPointerLeave={leave}
>
<div className={styles.shine} aria-hidden="true" />
<div className={styles.body}>
<span className={styles.badge}>{badge}</span>
<h3 className={styles.title}>{title}</h3>
</div>
</article>
);
}01 Card holographic mouse tilt — Measured area 13.972% · moving frames 20/23Original by GODRICH · SCSS and React ship in the zip · 9 CSS Holographic Card Effects (Copy-Paste)
The post this demo lives in
9 CSS Holographic Card Effects (Copy-Paste)A CSS holographic card effect fakes how foil catches light from different angles using only CSS gradients — the shine be See all demos in this post


