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 behind that Pokémon card look.
01 holographic-foil-card-mouse-tilt
02 holographic-foil-sticker-peel-shine
03 holographic-foil-badge-verified-ring
04 holographic-foil-text-iridescent
05 holographic-foil-ticket-stub-stamp
06 holographic-foil-authenticity-seal
07 holographic-foil-price-tag-ribbon
09 holographic-foil-loading-shimmer-badge
Auto-plays · click a tile to jump to its section · all nine in one zip
- 01 Card holographic mouse tilt
- 02 Sticker corner peel + holographic sweep
- 03 Verified badge holographic ring
- 04 Iridescent holographic text
- 05 Ticket stub holographic stamp
- 06 Authenticity seal (scalloped edge)
- 07 Price tag holographic ribbon
- 08 Avatar holographic status ring
- 09 Loading badge holographic shimmer
The order below isn't by popularity — it's the order a shiny surface actually shows up on a page. A product card catches the cursor and pulls focus first (01), holographic sticker css gets slapped on as decoration (02), a round badge backs up trust (03), a headline sets the mood (04), a ticket locks in a purchase (05), a seal proves it's real (06), a ribbon nudges toward buying (07), an avatar ring marks identity (08), and a loading badge fills the wait at the very end (09).
| # | Name | What moves | JS |
|---|---|---|---|
| 01 | Card holographic mouse tilt | highlight position, card rotateX/rotateY |
8 lines |
| 02 | Sticker corner peel + holographic sweep | corner transform, sheet background-position |
none |
| 03 | Verified badge holographic ring | ring conic-gradient spin |
none |
| 04 | Iridescent holographic text | filter: hue-rotate, background-position |
none |
| 05 | Ticket stub holographic stamp | stamp spin + scale pulse |
none |
| 06 | Authenticity seal | clip-path seal spin speed |
none |
| 07 | Price tag holographic ribbon | ribbon rotate/background-position |
none |
| 08 | Avatar holographic status ring | ring conic-gradient spin |
none |
| 09 | Loading badge holographic shimmer | diagonal band background-position, dot scale |
none |
01Card holographic mouse tilt
What moves
Hovering over the card tracks the cursor with a rainbow highlight sweeping across the surface while the whole card tilts toward it. It suits limited-edition product cards or merch pages that want that shiny trading-card look.

<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 · See the password
In the code
:hover alone can't read exact cursor coordinates, so 8 lines of pointermove update four custom properties — --mx, --my, --rx, --ry — and nothing else.
.holo-card {
transform: perspective(700px) rotateX(var(--rx)) rotateY(var(--ry));
transition: transform $duration $easing;
}
.holo-card__shine {
background: conic-gradient(from 0deg, #ff5ecb, #5ef1ff, #a3ff5e, #fff45e, #ff5ecb);
background-position: var(--mx) var(--my);
mix-blend-mode: color-dodge;
}
c.addEventListener('pointermove', function(e){
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');
});
02Sticker corner peel + holographic sweep
What moves
One corner of the sticker card sits slightly lifted at rest, and hovering sends a rainbow sheet sweeping diagonally underneath it.

<main class="stage">
<div class="sticker is-demo" tabindex="0">
<span class="sticker__peel" aria-hidden="true"></span>
<span class="sticker__shine" aria-hidden="true"></span>
<span class="sticker__flag">NEW</span>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-base; // 모서리가 더 들리는 시간
$easing: $ease-out;
$color: $subject-cream; // 스티커 바탕색
@include stage($stage-yellow);
.sticker {
position: relative;
width: min(150px, 48vw);
aspect-ratio: 1;
border-radius: $r-card;
background: $color;
box-shadow: $shadow-press;
overflow: hidden;
outline: none;
cursor: pointer;
}
.sticker__peel {
position: absolute; top: 0; right: 0;
width: 40px; height: 40px;
background: linear-gradient(135deg, rgba($stage-ink, .08) 50%, $stage-paper 50%);
clip-path: polygon(100% 0, 0 0, 100% 100%);
transform-origin: 100% 0; // 규칙8: 스티커 접힌 코너 경첩
box-shadow: -6px 6px 12px rgba($stage-ink, .28);
}
.sticker__shine {
position: absolute; inset: 0;
background: repeating-linear-gradient(115deg,
transparent 0 10%, #dcb9d1 10% 12%, #b9d9dc 12% 14%, #c8dcb9 14% 16%, #dcdab9 16% 18%, transparent 18% 30%);
background-size: 260% 260%;
background-position: -60% -60%;
mix-blend-mode: multiply;
opacity: 0;
}
.sticker__flag {
position: absolute; left: $sp-3; bottom: $sp-3;
font-weight: 800; font-size: 12px; letter-spacing: .04em; color: $stage-ink; font-family: inherit;
}
.sticker:hover .sticker__peel,
.sticker:focus-visible .sticker__peel { transform: rotate(-16deg) translate(6px, -6px); }
.sticker:hover .sticker__shine,
.sticker:focus-visible .sticker__shine { opacity: .9; background-position: 60% 60%; transition: opacity $duration $easing, background-position $duration $easing; }
.sticker.is-demo .sticker__peel { animation: peel-loop $dur-loop $easing infinite; }
.sticker.is-demo .sticker__shine { animation: sweep-loop $dur-loop $easing infinite; }
@keyframes peel-loop {
0%, 10% { transform: rotate(-6deg) translate(2px, -2px); }
40% { transform: rotate(-18deg) translate(7px, -7px); }
70% { transform: rotate(-13deg) translate(5px, -5px); }
90%, 100% { transform: rotate(-6deg) translate(2px, -2px); }
}
@keyframes sweep-loop {
0%, 10% { opacity: 0; background-position: -60% -60%; }
40% { opacity: .9; background-position: 20% 20%; }
70% { opacity: .9; background-position: 90% 90%; }
90%, 100% { opacity: 0; background-position: 150% 150%; }
}
@media (prefers-reduced-motion: reduce) {
.sticker__peel { transform: rotate(-12deg) translate(4px, -4px); }
.sticker__shine { opacity: .5; background-position: 40% 40%; }
.sticker.is-demo .sticker__peel,
.sticker.is-demo .sticker__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: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.sticker {
position: relative;
width: min(150px, 48vw);
aspect-ratio: 1;
border-radius: 16px;
background: #fff7e6;
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
overflow: hidden;
outline: none;
cursor: pointer;
}
.sticker__peel {
position: absolute;
top: 0;
right: 0;
width: 40px;
height: 40px;
background: linear-gradient(135deg, rgba(23, 20, 26, 0.08) 50%, #ffffff 50%);
clip-path: polygon(100% 0, 0 0, 100% 100%);
transform-origin: 100% 0;
box-shadow: -6px 6px 12px rgba(23, 20, 26, 0.28);
}
.sticker__shine {
position: absolute;
inset: 0;
background: repeating-linear-gradient(115deg, transparent 0 10%, #dcb9d1 10% 12%, #b9d9dc 12% 14%, #c8dcb9 14% 16%, #dcdab9 16% 18%, transparent 18% 30%);
background-size: 260% 260%;
background-position: -60% -60%;
mix-blend-mode: multiply;
opacity: 0;
}
.sticker__flag {
position: absolute;
left: 12px;
bottom: 12px;
font-weight: 800;
font-size: 12px;
letter-spacing: 0.04em;
color: #17141a;
font-family: inherit;
}
.sticker:hover .sticker__peel,
.sticker:focus-visible .sticker__peel {
transform: rotate(-16deg) translate(6px, -6px);
}
.sticker:hover .sticker__shine,
.sticker:focus-visible .sticker__shine {
opacity: 0.9;
background-position: 60% 60%;
transition: opacity 300ms ease-out, background-position 300ms ease-out;
}
.sticker.is-demo .sticker__peel {
animation: peel-loop 2s ease-out infinite;
}
.sticker.is-demo .sticker__shine {
animation: sweep-loop 2s ease-out infinite;
}
@keyframes peel-loop {
0%, 10% {
transform: rotate(-6deg) translate(2px, -2px);
}
40% {
transform: rotate(-18deg) translate(7px, -7px);
}
70% {
transform: rotate(-13deg) translate(5px, -5px);
}
90%, 100% {
transform: rotate(-6deg) translate(2px, -2px);
}
}
@keyframes sweep-loop {
0%, 10% {
opacity: 0;
background-position: -60% -60%;
}
40% {
opacity: 0.9;
background-position: 20% 20%;
}
70% {
opacity: 0.9;
background-position: 90% 90%;
}
90%, 100% {
opacity: 0;
background-position: 150% 150%;
}
}
@media (prefers-reduced-motion: reduce) {
.sticker__peel {
transform: rotate(-12deg) translate(4px, -4px);
}
.sticker__shine {
opacity: 0.5;
background-position: 40% 40%;
}
.sticker.is-demo .sticker__peel,
.sticker.is-demo .sticker__shine {
animation: none;
}
}import type { CSSProperties } from "react";
import styles from "./StickerPeelShine.module.scss";
export type StickerPeelShineProps = {
flag: string;
/** 모서리가 더 들리는 시간 (기본 300ms) */
duration?: number;
easing?: string;
/** 스티커 바탕색 */
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function StickerPeelShine({
flag,
duration = 300,
easing = "ease-out",
color = "#fff7e6",
}: StickerPeelShineProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.sticker} style={vars} tabIndex={0}>
<span className={styles.peel} aria-hidden="true" />
<span className={styles.shine} aria-hidden="true" />
<span className={styles.flag}>{flag}</span>
</div>
);
}02 · Sticker corner peel + holographic sweep — Measured area 15.39% · moving frames 19/23Original by GODRICH · SCSS and React ship in the zip · See the password
In the code
It fits planner or diary sticker mockups and badge-style product sticker previews.
.sticker__peel {
clip-path: polygon(100% 0, 0 0, 100% 100%);
transform-origin: 100% 0;
transform: rotate(-6deg) translate(2px, -2px);
}
.sticker:hover .sticker__peel {
transform: rotate(-16deg) translate(6px, -6px);
}
.sticker__shine {
background: repeating-linear-gradient(115deg, transparent 0 10%,
#ff5ecb 10% 12%, #5ef1ff 12% 14%, #a3ff5e 14% 16%, #fff45e 16% 18%, transparent 18% 30%);
background-size: 260% 260%;
}
03Verified badge holographic ring
What moves
A rainbow light continuously circles the badge's rim without pausing. It works for authenticity badges, verified-seller marks, and limited-run merch stamps.

<main class="stage">
<div class="v-badge">
<span class="v-badge__ring" aria-hidden="true"></span>
<span class="v-badge__inner">
<span class="v-badge__check" aria-hidden="true">✓</span>
<span class="v-badge__label">Verified</span>
</span>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 링이 한 바퀴 도는 시간
$easing: linear;
$color: $subject-blue; // 체크 아이콘 색
@include stage($stage-orange);
.v-badge {
position: relative;
width: min(120px, 40vw);
aspect-ratio: 1;
display: grid;
place-items: center;
}
.v-badge__ring {
position: absolute; inset: 0;
border-radius: $r-pill;
background: conic-gradient(from 0deg, #dcb9d1, #b9d9dc, #c8dcb9, #dcdab9, #dcb9d1);
-webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 8px), #000 calc(100% - 8px));
mask: radial-gradient(farthest-side, transparent calc(100% - 8px), #000 calc(100% - 8px));
animation: ring-spin $duration $easing infinite;
}
.v-badge__inner {
position: relative; z-index: 1;
width: 74%; aspect-ratio: 1;
border-radius: $r-pill;
background: $stage-paper;
box-shadow: $shadow-press;
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: $sp-1;
}
.v-badge__check { font-size: 22px; line-height: 1; color: $color; }
.v-badge__label { font-size: 10px; font-weight: 700; color: $stage-ink; font-family: inherit; }
@keyframes ring-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
.v-badge__ring { animation: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.v-badge {
position: relative;
width: min(120px, 40vw);
aspect-ratio: 1;
display: grid;
place-items: center;
}
.v-badge__ring {
position: absolute;
inset: 0;
border-radius: 999px;
background: conic-gradient(from 0deg, #dcb9d1, #b9d9dc, #c8dcb9, #dcdab9, #dcb9d1);
-webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 8px), #000 calc(100% - 8px));
mask: radial-gradient(farthest-side, transparent calc(100% - 8px), #000 calc(100% - 8px));
animation: ring-spin 2s linear infinite;
}
.v-badge__inner {
position: relative;
z-index: 1;
width: 74%;
aspect-ratio: 1;
border-radius: 999px;
background: #ffffff;
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
}
.v-badge__check {
font-size: 22px;
line-height: 1;
color: #2f6df6;
}
.v-badge__label {
font-size: 10px;
font-weight: 700;
color: #17141a;
font-family: inherit;
}
@keyframes ring-spin {
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: reduce) {
.v-badge__ring {
animation: none;
}
}import type { CSSProperties } from "react";
import styles from "./VerifiedBadgeRing.module.scss";
export type VerifiedBadgeRingProps = {
label: string;
/** 링이 한 바퀴 도는 시간 (기본 2000ms) */
duration?: number;
easing?: string;
/** 체크 아이콘 색 */
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function VerifiedBadgeRing({
label,
duration = 2000,
easing = "linear",
color = "#2f6df6",
}: VerifiedBadgeRingProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.badge} style={vars}>
<span className={styles.ring} aria-hidden="true" />
<span className={styles.inner}>
<span className={styles.check} aria-hidden="true">✓</span>
<span className={styles.label}>{label}</span>
</span>
</div>
);
}03 · Verified badge holographic ring — Measured area 2.312% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password
In the code
mask-image keeps only a ring-shaped slice, and a conic-gradient shifts color by angle the way real foil film reflects.
.v-badge__ring {
background: conic-gradient(from 0deg, #ff5ecb, #5ef1ff, #a3ff5e, #fff45e, #ff5ecb);
mask: radial-gradient(farthest-side, transparent calc(100% - 8px), #000 calc(100% - 8px));
animation: ring-spin 2s linear infinite;
}
@keyframes ring-spin { to { transform: rotate(360deg); } }
04Iridescent holographic text
What moves
Rainbow-gradient text keeps cycling through filter: hue-rotate, mimicking how printed holographic foil letters shift color nonstop.

<main class="stage">
<h2 class="holo-text">Holographic Sale</h2>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 색이 한 바퀴 도는 시간
$easing: linear;
$color: $subject-blue; // 사용 안 함 — 그라디언트가 색을 대신한다(참고용)
@include stage($stage-paper);
.holo-text {
margin: 0;
font-weight: 800;
font-size: clamp(26px, 9vw, 38px);
line-height: 1.1;
font-family: inherit;
background: linear-gradient(90deg, #dcb9d1, #b9d9dc, #c8dcb9, #dcdab9, #dcb9d1);
background-size: 300% 100%;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
filter: hue-rotate(0deg);
animation: holo-hue $duration $easing infinite, holo-slide $duration $easing infinite;
}
@keyframes holo-hue { to { filter: hue-rotate(360deg); } }
@keyframes holo-slide {
0% { background-position: 0% 50%; }
100% { background-position: 300% 50%; }
}
@media (prefers-reduced-motion: reduce) {
.holo-text { animation: none; filter: hue-rotate(0deg); background-position: 0% 50%; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.holo-text {
margin: 0;
font-weight: 800;
font-size: clamp(26px, 9vw, 38px);
line-height: 1.1;
font-family: inherit;
background: linear-gradient(90deg, #dcb9d1, #b9d9dc, #c8dcb9, #dcdab9, #dcb9d1);
background-size: 300% 100%;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
filter: hue-rotate(0deg);
animation: holo-hue 2s linear infinite, holo-slide 2s linear infinite;
}
@keyframes holo-hue {
to {
filter: hue-rotate(360deg);
}
}
@keyframes holo-slide {
0% {
background-position: 0% 50%;
}
100% {
background-position: 300% 50%;
}
}
@media (prefers-reduced-motion: reduce) {
.holo-text {
animation: none;
filter: hue-rotate(0deg);
background-position: 0% 50%;
}
}import type { CSSProperties } from "react";
import styles from "./HoloTextIridescent.module.scss";
export type HoloTextIridescentProps = {
text: string;
/** 색이 한 바퀴 도는 시간 (기본 2000ms) */
duration?: number;
easing?: string;
/** 사용 안 함 — 그라디언트가 색을 대신한다 (참고용) */
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function HoloTextIridescent({
text,
duration = 2000,
easing = "linear",
color = "#2f6df6",
}: HoloTextIridescentProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return <h2 className={styles.text} style={vars}>{text}</h2>;
}04 · Iridescent holographic text — Measured area 2.844% · moving frames 16/23Original by GODRICH · SCSS and React ship in the zip · See the password
In the code
It reads well as an event page title, a limited-edition product name, or a holographic-concept banner.
.holo-text {
background: linear-gradient(90deg, #ff5ecb, #5ef1ff, #a3ff5e, #fff45e, #ff5ecb);
background-size: 300% 100%;
background-clip: text;
color: transparent;
animation: holo-hue 2s linear infinite, holo-slide 2s linear infinite;
}
@keyframes holo-hue { to { filter: hue-rotate(360deg); } }
@keyframes holo-slide { to { background-position: 300% 50%; } }
05Ticket stub holographic stamp
What moves
A round stamp on the ticket stub quietly spins via a conic-gradient, growing slightly brighter and bigger on hover. It suits concert or event ticket UIs and authenticity marks on digital admission tickets.

<main class="stage">
<div class="ticket">
<div class="ticket__main">
<span class="ticket__label">Concert Ticket</span>
</div>
<div class="ticket__stub">
<span class="ticket__stampwrap is-demo" tabindex="0">
<span class="ticket__stamp" aria-hidden="true"></span>
</span>
<span class="ticket__stamp-text">VIP</span>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 스탬프가 커지는 펄스 한 번의 시간
$easing: $ease-out;
$color: $subject-blue; // 사용 안 함(참고용)
@include stage($stage-ink);
.ticket {
display: flex;
width: min(220px, 66vw);
border-radius: $r-card;
overflow: hidden;
box-shadow: $shadow-raised;
}
.ticket__main {
flex: 1;
background: $stage-paper;
padding: $sp-4;
display: flex; align-items: center;
}
.ticket__label { font-weight: 700; font-size: 12px; color: $stage-ink; font-family: inherit; }
.ticket__stub {
position: relative;
width: 76px;
background: $stage-ink;
display: grid; place-items: center;
border-left: 2px dashed rgba($stage-paper, .35);
}
.ticket__stampwrap {
position: relative;
width: 46px; height: 46px;
outline: none;
transition: transform $duration $easing, filter $duration $easing;
}
.ticket__stamp {
position: absolute; inset: 0;
border-radius: $r-pill;
background: conic-gradient(from 0deg, #dcb9d1, #b9d9dc, #c8dcb9, #dcdab9, #dcb9d1);
filter: saturate(1.1);
animation: stamp-spin 3s linear infinite;
}
.ticket__stamp-text {
position: absolute;
font-size: 10px; font-weight: 800; color: $stage-ink;
background: $stage-paper; padding: $sp-1 $sp-2; border-radius: $r-xs;
}
.ticket__stampwrap:hover,
.ticket__stampwrap:focus-visible { transform: scale(1.15); filter: brightness(1.2); }
.ticket__stampwrap.is-demo { animation: stamp-pulse $dur-loop $easing infinite; }
@keyframes stamp-spin { to { transform: rotate(360deg); } }
@keyframes stamp-pulse {
0%, 20% { transform: scale(1); filter: brightness(1); }
50% { transform: scale(1.15); filter: brightness(1.25); }
80%, 100% { transform: scale(1); filter: brightness(1); }
}
@media (prefers-reduced-motion: reduce) {
.ticket__stampwrap { transition: none; }
.ticket__stamp { animation: none; }
.ticket__stampwrap.is-demo { 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);
}
.ticket {
display: flex;
width: min(220px, 66vw);
border-radius: 16px;
overflow: hidden;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
}
.ticket__main {
flex: 1;
background: #ffffff;
padding: 16px;
display: flex;
align-items: center;
}
.ticket__label {
font-weight: 700;
font-size: 12px;
color: #17141a;
font-family: inherit;
}
.ticket__stub {
position: relative;
width: 76px;
background: #17141a;
display: grid;
place-items: center;
border-left: 2px dashed rgba(255, 255, 255, 0.35);
}
.ticket__stampwrap {
position: relative;
width: 46px;
height: 46px;
outline: none;
transition: transform 2s ease-out, filter 2s ease-out;
}
.ticket__stamp {
position: absolute;
inset: 0;
border-radius: 999px;
background: conic-gradient(from 0deg, #dcb9d1, #b9d9dc, #c8dcb9, #dcdab9, #dcb9d1);
filter: saturate(1.1);
animation: stamp-spin 3s linear infinite;
}
.ticket__stamp-text {
position: absolute;
font-size: 10px;
font-weight: 800;
color: #17141a;
background: #ffffff;
padding: 4px 8px;
border-radius: 4px;
}
.ticket__stampwrap:hover,
.ticket__stampwrap:focus-visible {
transform: scale(1.15);
filter: brightness(1.2);
}
.ticket__stampwrap.is-demo {
animation: stamp-pulse 2s ease-out infinite;
}
@keyframes stamp-spin {
to {
transform: rotate(360deg);
}
}
@keyframes stamp-pulse {
0%, 20% {
transform: scale(1);
filter: brightness(1);
}
50% {
transform: scale(1.15);
filter: brightness(1.25);
}
80%, 100% {
transform: scale(1);
filter: brightness(1);
}
}
@media (prefers-reduced-motion: reduce) {
.ticket__stampwrap {
transition: none;
}
.ticket__stamp {
animation: none;
}
.ticket__stampwrap.is-demo {
animation: none;
}
}import type { CSSProperties } from "react";
import styles from "./TicketStubStamp.module.scss";
export type TicketStubStampProps = {
label: string;
stampText: string;
/** 스탬프가 커지는 펄스 한 번의 시간 (기본 2000ms) */
duration?: number;
easing?: string;
/** 사용 안 함(참고용) */
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function TicketStubStamp({
label,
stampText,
duration = 2000,
easing = "ease-out",
color = "#2f6df6",
}: TicketStubStampProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.ticket} style={vars}>
<div className={styles.main}>
<span className={styles.label}>{label}</span>
</div>
<div className={styles.stub}>
<span className={styles.stampwrap} tabIndex={0}>
<span className={styles.stamp} aria-hidden="true" />
</span>
<span className={styles.stampText}>{stampText}</span>
</div>
</div>
);
}05 · Ticket stub holographic stamp — Measured area 1.053% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password
In the code
The spin and the scale pulse each sit on a different element, so the two motions never fight over the same transform.
.stampwrap {
transition: transform $duration $easing;
}
.stampwrap:hover { transform: scale(1.15); filter: brightness(1.2); }
.stamp {
background: conic-gradient(from 0deg, #ff5ecb, #5ef1ff, #a3ff5e, #fff45e, #ff5ecb);
animation: spin 3s linear infinite;
}
06Authenticity seal (scalloped edge)
What moves
A star-scalloped edge is cut with clip-path and topped with a slowly spinning rainbow gradient; hovering speeds up the spin.

<main class="stage">
<div class="seal" tabindex="0">
<span class="seal__shape" aria-hidden="true"></span>
<span class="seal__text">Genuine</span>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: 700ms; // 호버 시 회전이 빨라지는 애니메이션 길이
$easing: linear;
$color: $subject-blue; // 사용 안 함(참고용)
@include stage($stage-yellow);
.seal {
position: relative;
width: min(116px, 40vw);
aspect-ratio: 1;
display: grid;
place-items: center;
outline: none;
cursor: pointer;
}
.seal__shape {
position: absolute; inset: 0;
clip-path: polygon(50% 0%, 61% 12%, 75% 6%, 78% 20%, 93% 20%, 90% 35%, 100% 43%, 90% 55%,
96% 68%, 82% 70%, 80% 85%, 65% 80%, 58% 93%, 50% 82%, 42% 93%, 35% 80%,
20% 85%, 18% 70%, 4% 68%, 10% 55%, 0% 43%, 10% 35%, 7% 20%, 22% 20%, 25% 6%, 39% 12%);
background: conic-gradient(from 0deg, #dcb9d1, #b9d9dc, #c8dcb9, #dcdab9, #dcb9d1);
animation: seal-spin $dur-loop linear infinite;
}
.seal__text {
position: relative; z-index: 1;
font-weight: 800; font-size: 13px; color: $stage-ink; font-family: inherit;
background: $stage-paper; padding: $sp-2 $sp-3; border-radius: $r-pill;
}
.seal:hover .seal__shape,
.seal:focus-visible .seal__shape,
.seal.is-demo .seal__shape { animation-duration: $duration; }
@keyframes seal-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
.seal__shape { animation: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.seal {
position: relative;
width: min(116px, 40vw);
aspect-ratio: 1;
display: grid;
place-items: center;
outline: none;
cursor: pointer;
}
.seal__shape {
position: absolute;
inset: 0;
clip-path: polygon(50% 0%, 61% 12%, 75% 6%, 78% 20%, 93% 20%, 90% 35%, 100% 43%, 90% 55%, 96% 68%, 82% 70%, 80% 85%, 65% 80%, 58% 93%, 50% 82%, 42% 93%, 35% 80%, 20% 85%, 18% 70%, 4% 68%, 10% 55%, 0% 43%, 10% 35%, 7% 20%, 22% 20%, 25% 6%, 39% 12%);
background: conic-gradient(from 0deg, #dcb9d1, #b9d9dc, #c8dcb9, #dcdab9, #dcb9d1);
animation: seal-spin 2s linear infinite;
}
.seal__text {
position: relative;
z-index: 1;
font-weight: 800;
font-size: 13px;
color: #17141a;
font-family: inherit;
background: #ffffff;
padding: 8px 12px;
border-radius: 999px;
}
.seal:hover .seal__shape,
.seal:focus-visible .seal__shape,
.seal.is-demo .seal__shape {
animation-duration: 700ms;
}
@keyframes seal-spin {
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: reduce) {
.seal__shape {
animation: none;
}
}import type { CSSProperties } from "react";
import styles from "./AuthenticitySeal.module.scss";
export type AuthenticitySealProps = {
text: string;
/** 호버 시 회전이 빨라지는 애니메이션 길이 (기본 700ms) */
duration?: number;
easing?: string;
/** 사용 안 함(참고용) */
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function AuthenticitySeal({
text,
duration = 700,
easing = "linear",
color = "#2f6df6",
}: AuthenticitySealProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.seal} style={vars} tabIndex={0}>
<span className={styles.shape} aria-hidden="true" />
<span className={styles.text}>{text}</span>
</div>
);
}06 · Authenticity seal (scalloped edge) — Measured area 6.797% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password
In the code
It fits genuine-product stickers, official merch seals, and quality-guarantee badges.
.seal__shape {
clip-path: polygon(50% 0%, 61% 12%, 75% 6%, 78% 20%, 93% 20%, 90% 35%, 100% 43%,
90% 55%, 96% 68%, 82% 70%, 80% 85%, 65% 80%, 58% 93%, 50% 82%);
background: conic-gradient(from 0deg, #ff5ecb, #5ef1ff, #a3ff5e, #fff45e, #ff5ecb);
animation: seal-spin 2s linear infinite;
}
.seal:hover .seal__shape { animation-duration: 700ms; }
07Price tag holographic ribbon
What moves
A ribbon draped over the price tag's corner glows like foil, wobbling slightly on hover as light sweeps across it.

<main class="stage">
<div class="tag is-demo" tabindex="0">
<span class="tag__ribbon" aria-hidden="true">NEW</span>
<div class="tag__body">
<span class="tag__price">$29</span>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-base; // 리본이 흔들리는 시간
$easing: $ease-out;
$color: $subject-blue; // 사용 안 함(참고용)
@include stage($stage-orange);
.tag {
position: relative;
width: min(140px, 46vw);
aspect-ratio: 4 / 3;
border-radius: $r-card;
background: $stage-paper;
box-shadow: $shadow-press;
overflow: hidden;
display: grid;
place-items: center;
outline: none;
cursor: pointer;
}
.tag__ribbon {
position: absolute; top: 14px; right: -34px;
width: 120px; padding: 4px 0;
text-align: center;
transform: rotate(35deg);
background: repeating-linear-gradient(115deg, #dcb9d1 0 10%, #b9d9dc 10% 20%, #c8dcb9 20% 30%, #dcdab9 30% 40%);
background-size: 220% 220%;
background-position: 0 0;
font-size: 10px; font-weight: 800; letter-spacing: .05em; color: $stage-ink; font-family: inherit;
box-shadow: 0 2px 6px rgba($stage-ink, .25);
transition: transform $duration $easing, background-position $duration $easing;
}
.tag__price { font-weight: 700; font-size: 15px; color: $stage-ink; font-family: inherit; }
.tag:hover .tag__ribbon,
.tag:focus-visible .tag__ribbon { transform: rotate(30deg) translateY(-2px); background-position: 100% 40%; }
.tag.is-demo .tag__ribbon { animation: ribbon-sway $dur-loop $easing infinite; }
@keyframes ribbon-sway {
0%, 15% { transform: rotate(35deg) translateY(0); background-position: 0 0; }
40% { transform: rotate(28deg) translateY(-3px); background-position: 100% 40%; }
65% { transform: rotate(38deg) translateY(2px); background-position: 0 100%; }
90%, 100% { transform: rotate(35deg) translateY(0); background-position: 0 0; }
}
@media (prefers-reduced-motion: reduce) {
.tag__ribbon { transition: none; }
.tag.is-demo .tag__ribbon { animation: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.tag {
position: relative;
width: min(140px, 46vw);
aspect-ratio: 4/3;
border-radius: 16px;
background: #ffffff;
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
overflow: hidden;
display: grid;
place-items: center;
outline: none;
cursor: pointer;
}
.tag__ribbon {
position: absolute;
top: 14px;
right: -34px;
width: 120px;
padding: 4px 0;
text-align: center;
transform: rotate(35deg);
background: repeating-linear-gradient(115deg, #dcb9d1 0 10%, #b9d9dc 10% 20%, #c8dcb9 20% 30%, #dcdab9 30% 40%);
background-size: 220% 220%;
background-position: 0 0;
font-size: 10px;
font-weight: 800;
letter-spacing: 0.05em;
color: #17141a;
font-family: inherit;
box-shadow: 0 2px 6px rgba(23, 20, 26, 0.25);
transition: transform 300ms ease-out, background-position 300ms ease-out;
}
.tag__price {
font-weight: 700;
font-size: 15px;
color: #17141a;
font-family: inherit;
}
.tag:hover .tag__ribbon,
.tag:focus-visible .tag__ribbon {
transform: rotate(30deg) translateY(-2px);
background-position: 100% 40%;
}
.tag.is-demo .tag__ribbon {
animation: ribbon-sway 2s ease-out infinite;
}
@keyframes ribbon-sway {
0%, 15% {
transform: rotate(35deg) translateY(0);
background-position: 0 0;
}
40% {
transform: rotate(28deg) translateY(-3px);
background-position: 100% 40%;
}
65% {
transform: rotate(38deg) translateY(2px);
background-position: 0 100%;
}
90%, 100% {
transform: rotate(35deg) translateY(0);
background-position: 0 0;
}
}
@media (prefers-reduced-motion: reduce) {
.tag__ribbon {
transition: none;
}
.tag.is-demo .tag__ribbon {
animation: none;
}
}import type { CSSProperties } from "react";
import styles from "./PriceTagRibbon.module.scss";
export type PriceTagRibbonProps = {
ribbon: string;
price: string;
/** 리본이 흔들리는 시간 (기본 300ms) */
duration?: number;
easing?: string;
/** 사용 안 함(참고용) */
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function PriceTagRibbon({
ribbon,
price,
duration = 300,
easing = "ease-out",
color = "#2f6df6",
}: PriceTagRibbonProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.tag} style={vars} tabIndex={0}>
<span className={styles.ribbon} aria-hidden="true">{ribbon}</span>
<div className={styles.body}>
<span className={styles.price}>{price}</span>
</div>
</div>
);
}07 · Price tag holographic ribbon — Measured area 1.608% · moving frames 19/23Original by GODRICH · SCSS and React ship in the zip · See the password
In the code
It suits discount price tags, new-arrival ribbon tags, and promo badges.
.tag__ribbon {
transform: rotate(35deg);
background: repeating-linear-gradient(115deg,
#ff5ecb 0 10%, #5ef1ff 10% 20%, #a3ff5e 20% 30%, #fff45e 30% 40%);
background-size: 220% 220%;
transition: transform $duration $easing, background-position $duration $easing;
}
.tag:hover .tag__ribbon { transform: rotate(30deg) translateY(-2px); background-position: 100% 40%; }
08Avatar holographic status ring
What moves
A conic-gradient ring slowly rotates around a profile avatar to flag a special state like "premium" or "live." It fits premium member avatar frames, live-broadcast indicators, and VIP profile badges.
<main class="stage">
<div class="avatar-wrap">
<span class="avatar-wrap__ring" aria-hidden="true"></span>
<span class="avatar-wrap__avatar" role="img" aria-label="Profile picture"></span>
<span class="avatar-wrap__tag">PRO</span>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 링이 한 바퀴 도는 시간
$easing: linear;
$color: $subject-blue; // 아바타 배경 그라디언트 색
@include stage($stage-paper);
.avatar-wrap {
position: relative;
width: min(108px, 36vw);
aspect-ratio: 1;
display: grid;
place-items: center;
}
.avatar-wrap__ring {
position: absolute; inset: 0;
border-radius: $r-pill;
background: conic-gradient(from 0deg, #dcb9d1, #b9d9dc, #c8dcb9, #dcdab9, #dcb9d1);
-webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 6px), #000 calc(100% - 6px));
mask: radial-gradient(farthest-side, transparent calc(100% - 6px), #000 calc(100% - 6px));
animation: ring-spin $duration $easing infinite;
}
.avatar-wrap__avatar {
position: relative; z-index: 1;
width: 76%; aspect-ratio: 1;
border-radius: $r-pill;
background: linear-gradient(155deg, $color 0%, $stage-ink 100%);
}
.avatar-wrap__tag {
position: absolute; bottom: -2px; right: 2px; z-index: 2;
font-size: 9px; font-weight: 800; color: $stage-ink;
background: $stage-paper; padding: $sp-1 $sp-2; border-radius: $r-pill;
box-shadow: $shadow-press;
}
@keyframes ring-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
.avatar-wrap__ring { animation: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.avatar-wrap {
position: relative;
width: min(108px, 36vw);
aspect-ratio: 1;
display: grid;
place-items: center;
}
.avatar-wrap__ring {
position: absolute;
inset: 0;
border-radius: 999px;
background: conic-gradient(from 0deg, #dcb9d1, #b9d9dc, #c8dcb9, #dcdab9, #dcb9d1);
-webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 6px), #000 calc(100% - 6px));
mask: radial-gradient(farthest-side, transparent calc(100% - 6px), #000 calc(100% - 6px));
animation: ring-spin 2s linear infinite;
}
.avatar-wrap__avatar {
position: relative;
z-index: 1;
width: 76%;
aspect-ratio: 1;
border-radius: 999px;
background: linear-gradient(155deg, #2f6df6 0%, #17141a 100%);
}
.avatar-wrap__tag {
position: absolute;
bottom: -2px;
right: 2px;
z-index: 2;
font-size: 9px;
font-weight: 800;
color: #17141a;
background: #ffffff;
padding: 4px 8px;
border-radius: 999px;
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
}
@keyframes ring-spin {
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: reduce) {
.avatar-wrap__ring {
animation: none;
}
}import type { CSSProperties } from "react";
import styles from "./AvatarStatusRing.module.scss";
export type AvatarStatusRingProps = {
alt: string;
tag: string;
/** 링이 한 바퀴 도는 시간 (기본 2000ms) */
duration?: number;
easing?: string;
/** 아바타 배경 그라디언트 색 */
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function AvatarStatusRing({
alt,
tag,
duration = 2000,
easing = "linear",
color = "#2f6df6",
}: AvatarStatusRingProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.wrap} style={vars}>
<span className={styles.ring} aria-hidden="true" />
<span className={styles.avatar} role="img" aria-label={alt} />
<span className={styles.tag}>{tag}</span>
</div>
);
}08 · Avatar holographic status ring — Measured area 1.451% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password
In the code
It's the same mask-image trick as item 03, just with a photo sitting inside instead of a checkmark.
.avatar-wrap__ring {
background: conic-gradient(from 0deg, #ff5ecb, #5ef1ff, #a3ff5e, #fff45e, #ff5ecb);
mask: radial-gradient(farthest-side, transparent calc(100% - 6px), #000 calc(100% - 6px));
animation: ring-spin 2s linear infinite;
}
@keyframes ring-spin { to { transform: rotate(360deg); } }
09Loading badge holographic shimmer
What moves
A rainbow diagonal band repeatedly sweeps across a "processing" badge, keeping it shimmering until the task finishes.

<main class="stage">
<div class="loading-badge">
<span class="loading-badge__shine" aria-hidden="true"></span>
<span class="loading-badge__dot" aria-hidden="true"></span>
<span class="loading-badge__label">Processing</span>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 시머 띠가 한 번 훑는 시간
$easing: linear;
$color: $subject-blue; // 점 색
@include stage($stage-ink);
.loading-badge {
position: relative;
overflow: hidden;
display: inline-flex;
align-items: center;
gap: $sp-2;
padding: $sp-2 $sp-4;
border-radius: $r-pill;
background: $stage-ink;
box-shadow: $shadow-press;
border: 1px solid rgba($stage-paper, .12);
}
.loading-badge__shine {
position: absolute; inset: 0;
background: repeating-linear-gradient(115deg,
transparent 0 12%, rgba(255, 94, 203, .55) 12% 16%, rgba(94, 241, 255, .55) 16% 20%,
rgba(163, 255, 94, .55) 20% 24%, transparent 24% 36%);
background-size: 260% 260%;
animation: shimmer-sweep $duration $easing infinite;
}
.loading-badge__dot {
position: relative; z-index: 1;
width: 8px; height: 8px; border-radius: $r-pill;
background: $color;
animation: dot-pulse $duration $ease-out infinite;
}
.loading-badge__label { position: relative; z-index: 1; font-size: 11px; font-weight: 700; color: $stage-paper; font-family: inherit; }
@keyframes shimmer-sweep {
0% { background-position: 0% 0%; }
100% { background-position: -260% -260%; }
}
@keyframes dot-pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.4); opacity: .6; }
}
@media (prefers-reduced-motion: reduce) {
.loading-badge__shine,
.loading-badge__dot { 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);
}
.loading-badge {
position: relative;
overflow: hidden;
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border-radius: 999px;
background: #17141a;
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
border: 1px solid rgba(255, 255, 255, 0.12);
}
.loading-badge__shine {
position: absolute;
inset: 0;
background: repeating-linear-gradient(115deg, transparent 0 12%, rgba(255, 94, 203, 0.55) 12% 16%, rgba(94, 241, 255, 0.55) 16% 20%, rgba(163, 255, 94, 0.55) 20% 24%, transparent 24% 36%);
background-size: 260% 260%;
animation: shimmer-sweep 2s linear infinite;
}
.loading-badge__dot {
position: relative;
z-index: 1;
width: 8px;
height: 8px;
border-radius: 999px;
background: #2f6df6;
animation: dot-pulse 2s ease-out infinite;
}
.loading-badge__label {
position: relative;
z-index: 1;
font-size: 11px;
font-weight: 700;
color: #ffffff;
font-family: inherit;
}
@keyframes shimmer-sweep {
0% {
background-position: 0% 0%;
}
100% {
background-position: -260% -260%;
}
}
@keyframes dot-pulse {
0%, 100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.4);
opacity: 0.6;
}
}
@media (prefers-reduced-motion: reduce) {
.loading-badge__shine,
.loading-badge__dot {
animation: none;
}
}import type { CSSProperties } from "react";
import styles from "./LoadingShimmerBadge.module.scss";
export type LoadingShimmerBadgeProps = {
label: string;
/** 시머 띠가 한 번 훑는 시간 (기본 2000ms) */
duration?: number;
easing?: string;
/** 점 색 */
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function LoadingShimmerBadge({
label,
duration = 2000,
easing = "linear",
color = "#2f6df6",
}: LoadingShimmerBadgeProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.badge} style={vars}>
<span className={styles.shine} aria-hidden="true" />
<span className={styles.dot} aria-hidden="true" />
<span className={styles.label}>{label}</span>
</div>
);
}09 · Loading badge holographic shimmer — Measured area 1.649% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password
In the code
It suits payment-processing badges, upload or sync progress indicators, and AI-generation waiting states.
.loading-badge__shine {
background: repeating-linear-gradient(115deg, transparent 0 12%,
rgba(255,94,203,.55) 12% 16%, rgba(94,241,255,.55) 16% 20%, rgba(163,255,94,.55) 20% 24%, transparent 24% 36%);
background-size: 260% 260%;
animation: shimmer-sweep 2s linear infinite;
}
@keyframes shimmer-sweep { to { background-position: -260% -260%; } }
Where this breaks
css holographic card — where does it break?
mix-blend-mode: color-dodge on item 01 gets harsher the lighter the card behind it — against a near-white background it can blow the whole card out to white, so the card needs a dark gradient ($stage-ink-ish) underneath to hold contrast. Items 03, 06, and 08 animate plain transform: rotate(), so they interpolate smoothly everywhere without @property; item 01's highlight only transitions background-position when a real cursor moves it; the grid preview's forced auto-play loop is a separate case that keyframes that same property directly instead of the raw --mx custom property, since animating a custom property in @keyframes without registering it via @property can render as choppy jumps on Safari below 16.4. The password for these nine files is 5tktgjj6, written into this sentence the same plain way as the rest of the paragraph.
Accessibility
What we checked
All nine turn off their loops and transitions under prefers-reduced-motion: reduce and settle near their resting look. 01 drops the tilt and highlight transitions, leaving a soft static highlight; 02 stays with its corner already lifted; 03 and 08 stop spinning; 04 freezes on one color stop of the gradient; 05, 06, 07, and 09 stop their spin, sweep, or pulse and leave a static shape. Since 01's pointermove tilt never fires for keyboard users in the first place, :focus-visible gets its own fallback that lights up the highlight without the tilt. Browser support for mix-blend-mode is documented on MDN. If you want more card-hover ideas, the earlier card hover collection and the text-gradient shine set sit in the same visual family.
FAQ
What happens to the 01 mouse-tilt css holographic effect on a touch device with no mouse?
pointermove fires for mouse, pen, and touch alike, so dragging a finger across the card still moves the highlight. For a plain tap with no drag, @media (hover: none) swaps in an :active fallback that lights up the highlight without the tilt.
Do the 03 and 08 ring animations need @property to look smooth?
No. Both animate transform: rotate() rather than a custom property directly, so every modern browser interpolates them smoothly with no extra registration. @property only matters when a custom property itself is the value being keyframed.
Does the React version of item 01 use state?
No. HoloCardMouseTilt skips useState and grabs the DOM node with useRef, updating the custom properties through style.setProperty directly. Re-rendering the React tree on every pointer move would cost more than just touching the CSS values.









