01Nghiêng thẻ theo con trỏ kiểu holographic
Khi rê chuột qua thẻ, một vệt sáng cầu vồng chạy theo vị trí con trỏ và cả thẻ nghiêng nhẹ theo hướng đó.
Dùng choThẻ sản phẩm giới hạn, trang chi tiết merch muốn có vẻ lấp lánh như thẻ bài sưu tầm
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">Phiên bản giới hạn</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 Nghiêng thẻ theo con trỏ kiểu holographic — Diện tích đo được 13.972% · khung hình chuyển động 20/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · 9 hiệu ứng CSS Holographic Card Effect copy-paste
Bài viết chứa demo này
9 hiệu ứng CSS Holographic Card Effect copy-pasteHiệu ứng css holographic card effect giả lập cách lá thiếc phản chiếu ánh sáng theo góc nhìn, chỉ bằng CSS — lấp lánh nh Xem tất cả demo trong bài này


