CSS ホログラムカード効果9選 — ステッカー・バッジ・チケット
CSS ホログラムとは、角度によって光の反射が変わる箔素材を、CSSのグラデーションとアニメーションだけで再現する技法です。ポケモンカードのような輝きは何度も画面で見かけますが、いざコードにしようとすると手が止まることが多いので、カード・ステッカー・バッジ・チケットなど9種類を実際にレンダリングしてまとめました。
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
自動再生 · タイルを押すとその項目へ · すべて1つのzip
- 01 カードのホログラムマウスチルト
- 02 ステッカーの角めくれ+ホログラムスイープ
- 03 認証バッジのホログラムリング
- 04 ホログラムテキスト(虹色回転)
- 05 チケット半券のホログラムスタンプ
- 06 正規品認証シール(ギザギザ縁)
- 07 価格タグのホログラムリボン
- 08 アバターのホログラムステータスリング
- 09 ローディングバッジのホログラムシマー
9個は人気順ではなく、ホログラム素材が実際の画面に登場する順番で並べています。まず商品カードがカーソルを追って輝き視線を引きつけ(01)、ステッカーが装飾として貼られ(02)、丸いバッジが信頼を裏づけ(03)、見出しテキストが雰囲気を作った後(04)、チケットが購入を確定し(05)、正規品シールが本物であることを証明し(06)、価格タグのリボンが購入を後押しし(07)、アバターのリングがユーザーの立場を示し(08)、最後にローディングバッジが待ち時間の画面を埋めます(09)。
| No | 名前 | 何が動くか | JS |
|---|---|---|---|
| 01 | カードのホログラムマウスチルト | ハイライト位置、カードのrotateX・rotateY |
8行 |
| 02 | ステッカーの角めくれ+ホログラムスイープ | 角のtransform、シートのbackground-position |
なし |
| 03 | 認証バッジのホログラムリング | リングのconic-gradient回転 |
なし |
| 04 | ホログラムテキスト | filter: hue-rotate、background-position |
なし |
| 05 | チケット半券のホログラムスタンプ | スタンプの回転・scaleパルス |
なし |
| 06 | 正規品認証シール | clip-pathシールの回転速度 |
なし |
| 07 | 価格タグのホログラムリボン | リボンのrotate・background-position |
なし |
| 08 | アバターのホログラムステータスリング | リングのconic-gradient回転 |
なし |
| 09 | ローディングバッジのホログラムシマー | 斜めの帯のbackground-position、点のscale |
なし |
01カードのホログラムマウスチルト
何が動くか
カードにカーソルを乗せると、位置に合わせて虹色のハイライトが表面を走り、カード全体がその方向へわずかに傾きます。限定商品カードや、ポケモンカードのように見せたいグッズ詳細ページに向いています。

<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">限定版</span>
<h3 class="holo-card__title">ミッドナイト・オーロラ</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 · カードのホログラムマウスチルト — 計測 累積面積 13.972% · 動いたフレーム 20/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
コードで見る所
:hoverだけでは正確なカーソル座標を読み取れないため、pointermoveのわずか8行で--mx・--my・--rx・--ryの4つのカスタムプロパティだけを更新します。
.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');
});
02ステッカーの角めくれ+ホログラムスイープ
何が動くか
ステッカーカードの一角が普段から少しめくれていて、ホバーするとその下を虹色のシートが斜めに走り抜けます。

<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 · ステッカーの角めくれ+ホログラムスイープ — 計測 累積面積 15.39% · 動いたフレーム 19/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
コードで見る所
手帳・プランナーステッカーのモックアップや、バッジ型商品ステッカーのプレビューに使えます。
.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%;
}
03認証バッジのホログラムリング
何が動くか
円形バッジの縁を虹色の光が絶え間なく一周します。正規品認証バッジ、認証済み出品者マーク、限定グッズのスタンプに使います。

<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">認証済み</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 · 認証バッジのホログラムリング — 計測 累積面積 2.312% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
コードで見る所
mask-imageでリング状だけを残し、conic-gradientが角度ごとに色を変えるので、本物の箔フィルムの反射に近く見えます。
.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); } }
04ホログラムテキスト
何が動くか
文字に虹色グラデーションをかけてfilter: hue-rotateを回し続け、印刷されたホログラムステッカーの文字のように色が絶えず変わります。

<main class="stage">
<h2 class="holo-text">ホログラムセール</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 · ホログラムテキスト(虹色回転) — 計測 累積面積 2.844% · 動いたフレーム 16/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
コードで見る所
イベントページのタイトル、限定商品名、ホログラムコンセプトのバナーに向いています。
.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%; } }
05チケット半券のホログラムスタンプ
何が動くか
チケットの半券に押された丸いスタンプがconic-gradientで静かに回転し、ホバーするとわずかに大きく明るくなります。コンサート・イベントチケットのUIや、デジタル入場券の真正性表示に使えます。

<main class="stage">
<div class="ticket">
<div class="ticket__main">
<span class="ticket__label">コンサートチケット</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 · チケット半券のホログラムスタンプ — 計測 累積面積 1.053% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
コードで見る所
回転と拡大のアニメーションを別々の要素に分けているので、2つの動きが同じ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;
}
06正規品認証シール(ギザギザ縁)
何が動くか
星形のギザギザ縁をclip-pathで切り抜き、虹色グラデーションを重ねてゆっくり回転させます。 ホバーすると回転が速くなります。

<main class="stage">
<div class="seal" tabindex="0">
<span class="seal__shape" aria-hidden="true"></span>
<span class="seal__text">正規品</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 · 正規品認証シール(ギザギザ縁) — 計測 累積面積 6.797% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
コードで見る所
正規品認証ステッカー、公式グッズシール、品質保証バッジに使います。
.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; }
07価格タグのホログラムリボン
何が動くか
価格タグの角にかかったリボンが箔のように輝き、ホバーするとわずかに揺れながら光がリボンの上を走り抜けます。

<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">2,900円</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 · 価格タグのホログラムリボン — 計測 累積面積 1.608% · 動いたフレーム 19/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
コードで見る所
セール価格タグ、新着リボンタグ、プロモーションバッジに使います。
.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%; }
08アバターのホログラムステータスリング
何が動くか
プロフィールアバターの縁をconic-gradientのリングがゆっくり回転し、「プレミアム」「配信中」などの特別な状態を示します。プレミアム会員アバターの縁取り、配信中の表示、VIPプロフィールバッジに使います。
<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="プロフィール画像"></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 · アバターのホログラムステータスリング — 計測 累積面積 1.451% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
コードで見る所
03番の認証バッジと同じmask-imageの技法ですが、中にチェックマークの代わりに写真が入る点が違います。
.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); } }
09ローディングバッジのホログラムシマー
何が動くか
処理中を知らせるバッジの上を虹色の斜めの帯が繰り返し走り、終わるまで輝き続けます。

<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">処理中</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 · ローディングバッジのホログラムシマー — 計測 累積面積 1.649% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
コードで見る所
決済処理中バッジ、アップロード・同期の進行表示、AI生成待機状態に使います。
.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%; } }
どこで壊れるか(落とし穴)
01番で使うmix-blend-mode: color-dodgeは、カードの背景が暗いほど自然に見えますが、背景がほぼ白に近いとハイライトが白飛びしてしまいます。カード自体に暗めのグラデーション($stage-ink系)を敷いてコントラストを保つ必要があります。また03・06・08番のようにconic-gradientを回転させる項目は、アニメーション対象がtransform: rotate()だけなので@propertyの登録なしでもすべてのブラウザで滑らかに補間されますが、01番のハイライト移動は、実際にカーソルを動かしたときはbackground-positionをトランジションで動かす方式で、格子で自動再生されるプレビューのループだけは別に--mxというカスタムプロパティそのものをキーフレームに入れず、background-position自体をキーフレームで直接指定しました。カスタムプロパティを@property登録なしでキーフレーム化すると、Safari 16.4未満ではカクついて見える場合があるためです。この9ファイルの圧縮パスワードは5tktgjj6で、この文章の中にそのまま書いてあります。
アクセシビリティ
押さえた点
9個すべてprefers-reduced-motion: reduceでループとトランジションを止め、静止した見た目に落ち着きます。01は傾きとハイライトのトランジションが消えてハイライトだけが控えめに残り、02は角がめくれた状態のまま固定され、03と08はリングの回転が止まり、04はグラデーションが一つの色で止まり、05・06・07・09は回転・スイープ・パルスが止まって静的な形だけが残ります。01番のpointermoveによるチルトはキーボード操作では最初から発生しないため、:focus-visibleにはチルトなしでハイライトだけ点く代替動作を別に用意しました。mix-blend-modeの対応状況はMDNのドキュメントで確認できます。カードホバー効果をもっと見たい方は、以前公開したカードホバー効果のまとめやテキストグラデーションのまとめもあわせてご覧ください。
FAQ
01番のマウスチルトは、マウスのないタッチ端末ではどうなりますか?
pointermoveはマウス・ペン・タッチをすべて同じイベントとして受け取るため、指でカードをドラッグすればハイライトも追従します。ドラッグせずタップだけの場合は、@media (hover: none)で:active状態のときにチルトなしでハイライトだけが点く代替動作が入っています。
03・08番のリングアニメーションは@propertyがなくても滑らかに回りますか?
はい。どちらもアニメーション対象がカスタムプロパティではなくtransform: rotate()なので、@propertyの登録なしでも最新ブラウザ全体で滑らかに補間されます。@propertyが必要になるのは、カスタムプロパティ自体をキーフレームの値にする場合だけです。
React版で01番はstateを使いますか?
いいえ。HoloCardMouseTiltはuseStateを使わず、useRefでDOMを直接つかみstyle.setPropertyでカスタムプロパティを更新します。ポインターが動くたびに再レンダリングすると重くなるため、Reactツリーを再描画せずCSSの値だけを書き換える方式にしています。









