【コピペOK】css バッジ デザイン 9選 — NEW・SALE・認証バッジ
サムネイルや名前の横に小さく貼りついて、いまの状態を一目で伝えるラベルがバッジです。新着・セール・認証済み・完売など、写真を使わずコードだけで作れます。ここに載せたcss バッジ デザインはどれもコピペOKで、画像ファイルは一切要りません。
01 badge-new-ribbon
02 badge-sale-percent
03 badge-verified-check
04 badge-beta-pill
05 badge-live-pulse-dot
06 badge-count-notification
07 badge-trending-flame
08 badge-limited-stock
09 badge-sold-out-overlay
自動再生 · タイルを押すとその項目へ · すべて1つのzip
- 01 コーナーリボンバッジ
- 02 セールタグバッジ
- 03 認証チェックバッジ
- 04 ベータグラデーションピルバッジ
- 05 ライブパルスドットバッジ
- 06 通知カウントバッジ
- 07 トレンディングフレイムバッジ
- 08 在庫バーバッジ
- 09 完売ストライプオーバーレイ
9つは人気順ではなく、実際のショッピング画面でバッジが登場する順に並べています。最初に目に入る新着(01)と財布を動かすセール(02)から始まり、信頼を伝える認証(03)・まだ調整中だと知らせるベータ(04)へ進みます。続いて今まさに起きていることを示すライブ(05)・通知 一覧 ui のようにたまっていく通知(06)、人気を裏づけるトレンド(07)を経て、在庫が減っていく警告(08)と締切を告げる完売(09)で終わります。同じ小さな札を光が屈折する側へ伸ばせばCSS ホログラムカード効果9選になり、フォームの正誤を伝える側へ沈めればCSS バリデーション アニメーション9選になります。
01コーナーリボンバッジ
何が見えるか
カード角を横切るリボンをrotate(-45deg)で斜めに配置し、その上に重ねた白い光の帯(::after)をbackground-positionだけでスイープさせています。

<main class="stage is-demo">
<div class="card" aria-label="new arrival">
<div class="thumb"></div>
<div class="ribbon-wrap">
<span class="ribbon">NEW</span>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-loop; // 2s — 빛줄기 한 주기
$easing: $ease-out;
$color: #50a26e; // NEW 리본 그린
@include stage($stage-yellow);
.card {
position: relative;
width: 220px; height: 140px;
border-radius: $r-card;
overflow: hidden;
background: $subject-cream;
box-shadow: $shadow-raised;
}
.thumb {
position: absolute; inset: 0;
background: linear-gradient(135deg, rgba($subject-blue, .12), rgba($subject-blue, .04));
}
.ribbon-wrap {
position: absolute;
top: $sp-6; left: -$sp-10;
width: 160px;
transform: rotate(-45deg);
overflow: hidden;
}
.ribbon {
position: relative;
display: block;
padding: $sp-1 0;
background: $color;
color: $subject-ink;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-weight: 700;
font-size: 13px;
letter-spacing: .06em;
text-align: center;
box-shadow: $shadow-press;
}
// 빛줄기 ::after — background-position 만 움직인다 (레이아웃은 그대로)
.ribbon::after {
content: "";
position: absolute; inset: 0;
background: linear-gradient(115deg, transparent 15%, rgba(255, 255, 255, .7) 45%, transparent 75%);
background-size: 220% 100%;
background-position: 140% 0;
}
.stage.is-demo .ribbon::after {
animation: shine-sweep $duration $easing infinite;
}
@keyframes shine-sweep {
0%, 15% { background-position: 140% 0; }
55% { background-position: -40% 0; }
100% { background-position: -40% 0; }
}
@media (prefers-reduced-motion: reduce) {
.stage.is-demo .ribbon::after { animation: none; background-position: 30% 0; }
}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);
}
.card {
position: relative;
width: 220px;
height: 140px;
border-radius: 16px;
overflow: hidden;
background: #fff7e6;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
}
.thumb {
position: absolute;
inset: 0;
background: linear-gradient(135deg, rgba(47, 109, 246, 0.12), rgba(47, 109, 246, 0.04));
}
.ribbon-wrap {
position: absolute;
top: 24px;
left: -40px;
width: 160px;
transform: rotate(-45deg);
overflow: hidden;
}
.ribbon {
position: relative;
display: block;
padding: 4px 0;
background: #50a26e;
color: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-weight: 700;
font-size: 13px;
letter-spacing: 0.06em;
text-align: center;
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
}
.ribbon::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(115deg, transparent 15%, rgba(255, 255, 255, 0.7) 45%, transparent 75%);
background-size: 220% 100%;
background-position: 140% 0;
}
.stage.is-demo .ribbon::after {
animation: shine-sweep 2s ease-out infinite;
}
@keyframes shine-sweep {
0%, 15% {
background-position: 140% 0;
}
55% {
background-position: -40% 0;
}
100% {
background-position: -40% 0;
}
}
@media (prefers-reduced-motion: reduce) {
.stage.is-demo .ribbon::after {
animation: none;
background-position: 30% 0;
}
}import type { CSSProperties } from "react";
import styles from "./BadgeNewRibbon.module.scss";
export type BadgeNewRibbonProps = {
duration?: number;
easing?: string;
color?: string;
label?: string;
};
export default function BadgeNewRibbon({
duration = 2000,
easing = "ease-out",
color = "#16a34a",
label = "NEW",
}: BadgeNewRibbonProps) {
const vars: CSSProperties = { "--duration": `${duration}ms`, "--easing": easing, "--color": color } as CSSProperties;
return (
<div className={styles.card} style={vars} aria-label="new arrival">
<div className={styles.thumb} />
<div className={styles.ribbonWrap}>
<span className={styles.ribbon}>{label}</span>
</div>
</div>
);
}01 · コーナーリボンバッジ — 計測 累積面積 1.912% · 動いたフレーム 9/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
コードのどこを見るか
止まっていても視線を引き続ける新着ラベルになります。
.ribbon-wrap {
position: absolute;
top: $sp-6; left: -$sp-10;
transform: rotate(-45deg);
}
.ribbon::after {
background: linear-gradient(115deg, transparent 15%, rgba(255, 255, 255, .7) 45%, transparent 75%);
background-size: 220% 100%;
}
.stage.is-demo .ribbon::after {
animation: shine-sweep $duration $easing infinite;
}
@keyframes shine-sweep {
0%, 15% { background-position: 140% 0; }
55%, 100% { background-position: -40% 0; }
}
02セールタグバッジ
画面で起きること
clip-pathで切り抜いた本物の値札の形(穴つき)のバッジが、わずかに揺れながら拡大縮小を繰り返します。

<main class="stage is-demo">
<div class="tag" aria-label="30% off">
<span class="tag__hole"></span>
<span class="tag__text">-30%</span>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-loop; // 2s — 흔들림 한 주기
$easing: $ease-spring;
$color: $stage-orange; // 세일 오렌지레드
@include stage($stage-ink);
.tag {
position: relative;
width: 132px; height: 92px;
// 진짜 가격표 모양 — 오른쪽이 뾰족한 오각형
clip-path: polygon(0 0, 76% 0, 100% 50%, 76% 100%, 0 100%);
background: $color;
display: grid; place-items: center;
box-shadow: $shadow-raised;
transform-origin: 8% 50%; // 규칙8: 구멍 핀 고정 흔들림
}
.tag__hole {
position: absolute;
left: $sp-2; top: 50%;
width: $sp-2; height: $sp-2;
margin-top: -$sp-1;
border-radius: 50%;
background: $stage-ink;
}
.tag__text {
color: #fff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-weight: 800;
font-size: 26px;
letter-spacing: -.01em;
padding-left: $sp-2;
}
.stage.is-demo .tag {
animation: tag-swing $duration $easing infinite;
}
@keyframes tag-swing {
0%, 100% { transform: rotate(0deg) scale(1); box-shadow: $shadow-raised; }
30% { transform: rotate(-6deg) scale(1.05); box-shadow: 0 18px 30px rgba(23, 20, 26, .4); }
60% { transform: rotate(3deg) scale(1.02); }
80% { transform: rotate(-1deg) scale(1); }
}
@media (prefers-reduced-motion: reduce) {
.stage.is-demo .tag { 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);
}
.tag {
position: relative;
width: 132px;
height: 92px;
clip-path: polygon(0 0, 76% 0, 100% 50%, 76% 100%, 0 100%);
background: #ff4d1f;
display: grid;
place-items: center;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
transform-origin: 8% 50%;
}
.tag__hole {
position: absolute;
left: 8px;
top: 50%;
width: 8px;
height: 8px;
margin-top: -4px;
border-radius: 50%;
background: #17141a;
}
.tag__text {
color: #fff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-weight: 800;
font-size: 26px;
letter-spacing: -0.01em;
padding-left: 8px;
}
.stage.is-demo .tag {
animation: tag-swing 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes tag-swing {
0%, 100% {
transform: rotate(0deg) scale(1);
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
}
30% {
transform: rotate(-6deg) scale(1.05);
box-shadow: 0 18px 30px rgba(23, 20, 26, 0.4);
}
60% {
transform: rotate(3deg) scale(1.02);
}
80% {
transform: rotate(-1deg) scale(1);
}
}
@media (prefers-reduced-motion: reduce) {
.stage.is-demo .tag {
animation: none;
}
}import type { CSSProperties } from "react";
import styles from "./BadgeSalePercent.module.scss";
export type BadgeSalePercentProps = {
duration?: number;
easing?: string;
color?: string;
percent?: number;
};
export default function BadgeSalePercent({
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#ff4d1f",
percent = 30,
}: BadgeSalePercentProps) {
const vars: CSSProperties = { "--duration": `${duration}ms`, "--easing": easing, "--color": color } as CSSProperties;
return (
<div className={styles.tag} style={vars} aria-label={`${percent}% off`}>
<span className={styles.hole} />
<span className={styles.text}>-{percent}%</span>
</div>
);
}02 · セールタグバッジ — 計測 累積面積 3.387% · 動いたフレーム 22/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
コードの読み方
糸で吊り下げられた紙タグのように見えます。
.tag {
clip-path: polygon(0 0, 76% 0, 100% 50%, 76% 100%, 0 100%);
transform-origin: 8% 50%;
}
.stage.is-demo .tag {
animation: tag-swing $duration $easing infinite;
}
@keyframes tag-swing {
0%, 100% { transform: rotate(0deg) scale(1); }
30% { transform: rotate(-6deg) scale(1.05); }
60% { transform: rotate(3deg) scale(1.02); }
80% { transform: rotate(-1deg) scale(1); }
}
03認証チェックバッジ
どんな動きか
名前の横の認証バッジをPhosphorのSVGアイコン1つで置き、確定の瞬間にtransform: scaleが1 → 1.28 → 0.94 → 1と跳ねてから収まり、確定した感じを出します。

<main class="stage is-demo">
<div class="row">
<span class="avatar"></span>
<span class="name">
godrich
<svg class="check" viewBox="0 0 256 256" width="20" height="20" aria-label="verified account">
<g fill="currentColor"><path d="M232 128c0 12.51-17.82 21.95-22.68 33.69c-4.68 11.32 1.42 30.65-7.78 39.85s-28.53 3.1-39.85 7.78C150 214.18 140.5 232 128 232s-22-17.82-33.69-22.68c-11.32-4.68-30.65 1.42-39.85-7.78s-3.1-28.53-7.78-39.85C41.82 150 24 140.5 24 128s17.82-22 22.68-33.69c4.68-11.31-1.42-30.65 7.78-39.85s28.54-3.1 39.85-7.78C106.05 41.82 115.5 24 128 24s22 17.82 33.69 22.68c11.32 4.68 30.65-1.42 39.85 7.78s3.1 28.53 7.78 39.85C214.18 106.05 232 115.5 232 128" opacity=".2"/><path d="M225.86 102.82c-3.77-3.94-7.67-8-9.14-11.57c-1.36-3.27-1.44-8.69-1.52-13.94c-.15-9.76-.31-20.82-8-28.51s-18.75-7.85-28.51-8c-5.25-.08-10.67-.16-13.94-1.52c-3.56-1.47-7.63-5.37-11.57-9.14C146.28 23.51 138.44 16 128 16s-18.27 7.51-25.18 14.14c-3.94 3.77-8 7.67-11.57 9.14c-3.25 1.36-8.69 1.44-13.94 1.52c-9.76.15-20.82.31-28.51 8s-7.8 18.75-8 28.51c-.08 5.25-.16 10.67-1.52 13.94c-1.47 3.56-5.37 7.63-9.14 11.57C23.51 109.72 16 117.56 16 128s7.51 18.27 14.14 25.18c3.77 3.94 7.67 8 9.14 11.57c1.36 3.27 1.44 8.69 1.52 13.94c.15 9.76.31 20.82 8 28.51s18.75 7.85 28.51 8c5.25.08 10.67.16 13.94 1.52c3.56 1.47 7.63 5.37 11.57 9.14c6.9 6.63 14.74 14.14 25.18 14.14s18.27-7.51 25.18-14.14c3.94-3.77 8-7.67 11.57-9.14c3.27-1.36 8.69-1.44 13.94-1.52c9.76-.15 20.82-.31 28.51-8s7.85-18.75 8-28.51c.08-5.25.16-10.67 1.52-13.94c1.47-3.56 5.37-7.63 9.14-11.57c6.63-6.9 14.14-14.74 14.14-25.18s-7.51-18.27-14.14-25.18m-11.55 39.29c-4.79 5-9.75 10.17-12.38 16.52c-2.52 6.1-2.63 13.07-2.73 19.82c-.1 7-.21 14.33-3.32 17.43s-10.39 3.22-17.43 3.32c-6.75.1-13.72.21-19.82 2.73c-6.35 2.63-11.52 7.59-16.52 12.38S132 224 128 224s-9.15-4.92-14.11-9.69s-10.17-9.75-16.52-12.38c-6.1-2.52-13.07-2.63-19.82-2.73c-7-.1-14.33-.21-17.43-3.32s-3.22-10.39-3.32-17.43c-.1-6.75-.21-13.72-2.73-19.82c-2.63-6.35-7.59-11.52-12.38-16.52S32 132 32 128s4.92-9.15 9.69-14.11s9.75-10.17 12.38-16.52c2.52-6.1 2.63-13.07 2.73-19.82c.1-7 .21-14.33 3.32-17.43s10.39-3.22 17.43-3.32c6.75-.1 13.72-.21 19.82-2.73c6.35-2.63 11.52-7.59 16.52-12.38S124 32 128 32s9.15 4.92 14.11 9.69s10.17 9.75 16.52 12.38c6.1 2.52 13.07 2.63 19.82 2.73c7 .1 14.33.21 17.43 3.32s3.22 10.39 3.32 17.43c.1 6.75.21 13.72 2.73 19.82c2.63 6.35 7.59 11.52 12.38 16.52S224 124 224 128s-4.92 9.15-9.69 14.11m-40.65-43.77a8 8 0 0 1 0 11.32l-56 56a8 8 0 0 1-11.32 0l-24-24a8 8 0 0 1 11.32-11.32L112 148.69l50.34-50.35a8 8 0 0 1 11.32 0"/></g>
</svg>
</span>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-loop; // 2s — 그리기+팝 한 주기
$easing: $ease-pop;
$color: $subject-blue;
@include stage($stage-paper);
.row {
display: flex; align-items: center; gap: $sp-2;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.avatar {
width: $sp-10; height: $sp-10;
border-radius: 50%;
background: linear-gradient(135deg, $subject-blue, $subject-ink);
}
.name {
display: inline-flex; align-items: center; gap: $sp-1;
font-size: 20px; font-weight: 700; color: $subject-ink;
}
.check {
transform-origin: center;
color: $color; // Phosphor duotone: g fill="currentColor" 가 이 값을 물려받는다
}
.stage.is-demo .check {
animation: pop $duration $easing infinite;
}
@keyframes pop {
0%, 50% { transform: scale(1); }
62% { transform: scale(1.28); }
74% { transform: scale(0.94); }
86%, 100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
.stage.is-demo .check { 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);
}
.row {
display: flex;
align-items: center;
gap: 8px;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(135deg, #2f6df6, #17141a);
}
.name {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 20px;
font-weight: 700;
color: #17141a;
}
.check {
transform-origin: center;
color: #2f6df6;
}
.stage.is-demo .check {
animation: pop 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
@keyframes pop {
0%, 50% {
transform: scale(1);
}
62% {
transform: scale(1.28);
}
74% {
transform: scale(0.94);
}
86%, 100% {
transform: scale(1);
}
}
@media (prefers-reduced-motion: reduce) {
.stage.is-demo .check {
animation: none;
}
}import type { CSSProperties } from "react";
import styles from "./BadgeVerifiedCheck.module.scss";
export type BadgeVerifiedCheckProps = {
duration?: number;
easing?: string;
color?: string;
name?: string;
};
export default function BadgeVerifiedCheck({
duration = 2000,
easing = "cubic-bezier(.2,1.4,.4,1)",
color = "#2f6df6",
name = "godrich",
}: BadgeVerifiedCheckProps) {
const vars: CSSProperties = { "--duration": `${duration}ms`, "--easing": easing, "--color": color } as CSSProperties;
return (
<div className={styles.row} style={vars}>
<span className={styles.avatar} />
<span className={styles.name}>
{name}
<svg className={styles.check} viewBox="0 0 256 256" width={20} height={20} aria-label="verified account">
<g fill="currentColor"><path d="M232 128c0 12.51-17.82 21.95-22.68 33.69c-4.68 11.32 1.42 30.65-7.78 39.85s-28.53 3.1-39.85 7.78C150 214.18 140.5 232 128 232s-22-17.82-33.69-22.68c-11.32-4.68-30.65 1.42-39.85-7.78s-3.1-28.53-7.78-39.85C41.82 150 24 140.5 24 128s17.82-22 22.68-33.69c4.68-11.31-1.42-30.65 7.78-39.85s28.54-3.1 39.85-7.78C106.05 41.82 115.5 24 128 24s22 17.82 33.69 22.68c11.32 4.68 30.65-1.42 39.85 7.78s3.1 28.53 7.78 39.85C214.18 106.05 232 115.5 232 128" opacity=".2" /><path d="M225.86 102.82c-3.77-3.94-7.67-8-9.14-11.57c-1.36-3.27-1.44-8.69-1.52-13.94c-.15-9.76-.31-20.82-8-28.51s-18.75-7.85-28.51-8c-5.25-.08-10.67-.16-13.94-1.52c-3.56-1.47-7.63-5.37-11.57-9.14C146.28 23.51 138.44 16 128 16s-18.27 7.51-25.18 14.14c-3.94 3.77-8 7.67-11.57 9.14c-3.25 1.36-8.69 1.44-13.94 1.52c-9.76.15-20.82.31-28.51 8s-7.8 18.75-8 28.51c-.08 5.25-.16 10.67-1.52 13.94c-1.47 3.56-5.37 7.63-9.14 11.57C23.51 109.72 16 117.56 16 128s7.51 18.27 14.14 25.18c3.77 3.94 7.67 8 9.14 11.57c1.36 3.27 1.44 8.69 1.52 13.94c.15 9.76.31 20.82 8 28.51s18.75 7.85 28.51 8c5.25.08 10.67.16 13.94 1.52c3.56 1.47 7.63 5.37 11.57 9.14c6.9 6.63 14.74 14.14 25.18 14.14s18.27-7.51 25.18-14.14c3.94-3.77 8-7.67 11.57-9.14c3.27-1.36 8.69-1.44 13.94-1.52c9.76-.15 20.82-.31 28.51-8s7.85-18.75 8-28.51c.08-5.25.16-10.67 1.52-13.94c1.47-3.56 5.37-7.63 9.14-11.57c6.63-6.9 14.14-14.74 14.14-25.18s-7.51-18.27-14.14-25.18m-11.55 39.29c-4.79 5-9.75 10.17-12.38 16.52c-2.52 6.1-2.63 13.07-2.73 19.82c-.1 7-.21 14.33-3.32 17.43s-10.39 3.22-17.43 3.32c-6.75.1-13.72.21-19.82 2.73c-6.35 2.63-11.52 7.59-16.52 12.38S132 224 128 224s-9.15-4.92-14.11-9.69s-10.17-9.75-16.52-12.38c-6.1-2.52-13.07-2.63-19.82-2.73c-7-.1-14.33-.21-17.43-3.32s-3.22-10.39-3.32-17.43c-.1-6.75-.21-13.72-2.73-19.82c-2.63-6.35-7.59-11.52-12.38-16.52S32 132 32 128s4.92-9.15 9.69-14.11s9.75-10.17 12.38-16.52c2.52-6.1 2.63-13.07 2.73-19.82c.1-7 .21-14.33 3.32-17.43s10.39-3.22 17.43-3.32c6.75-.1 13.72-.21 19.82-2.73c6.35-2.63 11.52-7.59 16.52-12.38S124 32 128 32s9.15 4.92 14.11 9.69s10.17 9.75 16.52 12.38c6.1 2.52 13.07 2.63 19.82 2.73c7 .1 14.33.21 17.43 3.32s3.22 10.39 3.32 17.43c.1 6.75.21 13.72 2.73 19.82c2.63 6.35 7.59 11.52 12.38 16.52S224 124 224 128s-4.92 9.15-9.69 14.11m-40.65-43.77a8 8 0 0 1 0 11.32l-56 56a8 8 0 0 1-11.32 0l-24-24a8 8 0 0 1 11.32-11.32L112 148.69l50.34-50.35a8 8 0 0 1 11.32 0" /></g>
</svg>
</span>
</div>
);
}03 · 認証チェックバッジ — 計測 累積面積 0.231% · 動いたフレーム 6/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
どこを変えるか
.check {
transform-origin: center;
color: $color; /* Phosphor duotone の fill="currentColor" がこの値を受け継ぐ */
}
.stage.is-demo .check {
animation: pop $duration $easing infinite;
}
@keyframes pop {
0%, 50% { transform: scale(1); }
62% { transform: scale(1.28); }
74% { transform: scale(0.94); }
86%, 100% { transform: scale(1); }
}
04ベータグラデーションピルバッジ
何が見えるか
紫色のグラデーションレイヤーをピルの背景色に重ね、opacityだけを0から1、また0へと上下させます。

<main class="stage is-demo">
<span class="pill" aria-label="beta feature">
<span class="pill__glow"></span>
<span class="pill__text">BETA</span>
</span>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-loop; // 2s — 색이 번갈아 드러나는 한 주기
$easing: $ease-out;
$color: $subject-blue;
@include stage($stage-orange);
.pill {
position: relative;
display: inline-block;
padding: $sp-2 $sp-6;
border-radius: $r-pill;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-weight: 800;
font-size: 22px;
letter-spacing: .08em;
color: #fff;
background: $color;
box-shadow: $shadow-raised;
overflow: hidden;
}
// 보라 레이어를 겹쳐 두고 opacity 만 오르내린다 — background-position 은
// 리페인트가 스크린샷 타이밍과 안 맞아 프레임 사이 변화가 문턱 아래로 묻혔다 (213 실측)
.pill__glow {
position: absolute; inset: 0;
background: linear-gradient(90deg, $stage-yellow, #c3add7);
opacity: 0;
}
.pill__text {
position: relative;
}
.stage.is-demo .pill__glow {
animation: glow-cross $duration $easing infinite;
}
@keyframes glow-cross {
0%, 10% { opacity: 0; }
50% { opacity: 1; }
90%, 100% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.stage.is-demo .pill__glow { animation: none; opacity: .5; }
}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);
}
.pill {
position: relative;
display: inline-block;
padding: 8px 24px;
border-radius: 999px;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-weight: 800;
font-size: 22px;
letter-spacing: 0.08em;
color: #fff;
background: #2f6df6;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
overflow: hidden;
}
.pill__glow {
position: absolute;
inset: 0;
background: linear-gradient(90deg, #ffd23f, #c3add7);
opacity: 0;
}
.pill__text {
position: relative;
}
.stage.is-demo .pill__glow {
animation: glow-cross 2s ease-out infinite;
}
@keyframes glow-cross {
0%, 10% {
opacity: 0;
}
50% {
opacity: 1;
}
90%, 100% {
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.stage.is-demo .pill__glow {
animation: none;
opacity: 0.5;
}
}import type { CSSProperties } from "react";
import styles from "./BadgeBetaPill.module.scss";
export type BadgeBetaPillProps = {
duration?: number;
easing?: string;
color?: string;
label?: string;
};
export default function BadgeBetaPill({
duration = 2000,
easing = "ease-out",
color = "#2f6df6",
label = "BETA",
}: BadgeBetaPillProps) {
const vars: CSSProperties = { "--duration": `${duration}ms`, "--easing": easing, "--color": color } as CSSProperties;
return (
<span className={styles.pill} style={vars} aria-label="beta feature">
<span className={styles.glow} />
<span className={styles.text}>{label}</span>
</span>
);
}04 · ベータグラデーションピルバッジ — 計測 累積面積 2.951% · 動いたフレーム 16/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
コードのどこを見るか
2色が溶け合うようにBETAラベルの上で交差します。
.pill { position: relative; overflow: hidden; }
.pill__glow {
position: absolute; inset: 0;
background: linear-gradient(90deg, $stage-yellow, #a855f7);
opacity: 0;
}
.stage.is-demo .pill__glow {
animation: glow-cross $duration $easing infinite;
}
@keyframes glow-cross {
0%, 10% { opacity: 0; }
50% { opacity: 1; }
90%, 100% { opacity: 0; }
}
05ライブパルスドットバッジ
画面で起きること
ドットの背後にある::before円が広がりながらopacityが0まで薄れることを繰り返します。

<main class="stage is-demo">
<span class="badge" aria-label="live now">
<span class="dot"></span>
LIVE
</span>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-loop; // 2s — 핑 한 주기
$easing: $ease-out;
$color: $error;
@include stage($stage-ink);
.badge {
display: inline-flex; align-items: center; gap: $sp-2;
padding: $sp-2 $sp-4;
border-radius: $r-pill;
background: rgba($stage-paper, .08);
color: $stage-paper;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-weight: 700;
font-size: 18px;
letter-spacing: .1em;
}
.dot {
position: relative;
width: $sp-2; height: $sp-2;
border-radius: 50%;
background: $color;
}
.dot::before {
content: "";
position: absolute; inset: 0;
border-radius: 50%;
background: $color;
}
.stage.is-demo .dot::before {
animation: ping $duration $easing infinite;
}
@keyframes ping {
0% { transform: scale(1); opacity: .7; }
70%, 100% { transform: scale(2.6); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.stage.is-demo .dot::before { animation: none; opacity: 0; }
}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);
}
.badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.08);
color: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-weight: 700;
font-size: 18px;
letter-spacing: 0.1em;
}
.dot {
position: relative;
width: 8px;
height: 8px;
border-radius: 50%;
background: #c2361a;
}
.dot::before {
content: "";
position: absolute;
inset: 0;
border-radius: 50%;
background: #c2361a;
}
.stage.is-demo .dot::before {
animation: ping 2s ease-out infinite;
}
@keyframes ping {
0% {
transform: scale(1);
opacity: 0.7;
}
70%, 100% {
transform: scale(2.6);
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.stage.is-demo .dot::before {
animation: none;
opacity: 0;
}
}import type { CSSProperties } from "react";
import styles from "./BadgeLivePulseDot.module.scss";
export type BadgeLivePulseDotProps = {
duration?: number;
easing?: string;
color?: string;
label?: string;
};
export default function BadgeLivePulseDot({
duration = 2000,
easing = "ease-out",
color = "#c2361a",
label = "LIVE",
}: BadgeLivePulseDotProps) {
const vars: CSSProperties = { "--duration": `${duration}ms`, "--easing": easing, "--color": color } as CSSProperties;
return (
<span className={styles.badge} style={vars} aria-label="live now">
<span className={styles.dot} />
{label}
</span>
);
}05 · ライブパルスドットバッジ — 計測 累積面積 0.147% · 動いたフレーム 8/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
コードの読み方
ライブ配信の発信源から信号が広がっていくレーダーのように見えます。
.dot { position: relative; }
.dot::before {
content: "";
position: absolute; inset: 0;
border-radius: 50%;
background: $color;
}
.stage.is-demo .dot::before {
animation: ping $duration $easing infinite;
}
@keyframes ping {
0% { transform: scale(1); opacity: .7; }
70%, 100% { transform: scale(2.6); opacity: 0; }
}
06通知カウントバッジ
トースト通知 css とどんな動きが違うか
小さな丸がscale(0)から1.15を経て1に収まるようにバウンドし、ちょうどそのタイミングでベルアイコン自体も短くrotateで揺れます。

<main class="stage is-demo">
<div class="bell-wrap" aria-label="5 new notifications">
<svg class="bell" viewBox="0 0 256 256" width="40" height="40" aria-hidden="true">
<g fill="currentColor"><path d="M208 192H48a8 8 0 0 1-6.88-12C47.71 168.6 56 139.81 56 104a72 72 0 0 1 144 0c0 35.82 8.3 64.6 14.9 76a8 8 0 0 1-6.9 12" opacity=".2"/><path d="M221.8 175.94c-5.55-9.56-13.8-36.61-13.8-71.94a80 80 0 1 0-160 0c0 35.34-8.26 62.38-13.81 71.94A16 16 0 0 0 48 200h40.81a40 40 0 0 0 78.38 0H208a16 16 0 0 0 13.8-24.06M128 216a24 24 0 0 1-22.62-16h45.24A24 24 0 0 1 128 216m-80-32c7.7-13.24 16-43.92 16-80a64 64 0 1 1 128 0c0 36.05 8.28 66.73 16 80Z"/></g>
</svg>
<span class="count">5</span>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-loop; // 2s — 흔들림+팝 한 주기
$easing: $ease-pop;
$color: $error;
@include stage($stage-ink);
.bell-wrap {
position: relative;
width: 64px; height: 64px;
display: grid; place-items: center;
}
.bell {
transform-origin: 50% 12%; // 규칙8: 종 꼭지점 흔들림
color: $stage-yellow; // Phosphor duotone: g fill="currentColor" 가 이 값을 물려받는다
}
.count {
position: absolute;
top: 2px; right: 0;
min-width: $sp-4; height: $sp-4;
border-radius: $r-pill;
background: $color;
color: #fff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-weight: 700;
font-size: 11px;
display: grid; place-items: center;
padding: 0 $sp-1;
box-shadow: 0 0 0 2px $stage-ink;
transform: scale(0);
}
.stage.is-demo .bell {
animation: shake $duration $easing infinite;
}
.stage.is-demo .count {
animation: pop-in $duration $easing infinite;
}
@keyframes shake {
0%, 55%, 100% { transform: rotate(0deg); }
60% { transform: rotate(-12deg); }
65% { transform: rotate(10deg); }
70% { transform: rotate(-7deg); }
75% { transform: rotate(4deg); }
80% { transform: rotate(0deg); }
}
@keyframes pop-in {
0%, 52% { transform: scale(0); }
62% { transform: scale(1.15); }
74%, 100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
.stage.is-demo .bell { animation: none; }
.stage.is-demo .count { animation: none; transform: scale(1); }
}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);
}
.bell-wrap {
position: relative;
width: 64px;
height: 64px;
display: grid;
place-items: center;
}
.bell {
transform-origin: 50% 12%;
color: #ffd23f;
}
.count {
position: absolute;
top: 2px;
right: 0;
min-width: 16px;
height: 16px;
border-radius: 999px;
background: #c2361a;
color: #fff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-weight: 700;
font-size: 11px;
display: grid;
place-items: center;
padding: 0 4px;
box-shadow: 0 0 0 2px #17141a;
transform: scale(0);
}
.stage.is-demo .bell {
animation: shake 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
.stage.is-demo .count {
animation: pop-in 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
@keyframes shake {
0%, 55%, 100% {
transform: rotate(0deg);
}
60% {
transform: rotate(-12deg);
}
65% {
transform: rotate(10deg);
}
70% {
transform: rotate(-7deg);
}
75% {
transform: rotate(4deg);
}
80% {
transform: rotate(0deg);
}
}
@keyframes pop-in {
0%, 52% {
transform: scale(0);
}
62% {
transform: scale(1.15);
}
74%, 100% {
transform: scale(1);
}
}
@media (prefers-reduced-motion: reduce) {
.stage.is-demo .bell {
animation: none;
}
.stage.is-demo .count {
animation: none;
transform: scale(1);
}
}import type { CSSProperties } from "react";
import styles from "./BadgeCountNotification.module.scss";
export type BadgeCountNotificationProps = {
duration?: number;
easing?: string;
color?: string;
count?: number;
};
export default function BadgeCountNotification({
duration = 2000,
easing = "cubic-bezier(.2,1.4,.4,1)",
color = "#c2361a",
count = 5,
}: BadgeCountNotificationProps) {
const vars: CSSProperties = { "--duration": `${duration}ms`, "--easing": easing, "--color": color } as CSSProperties;
return (
<div className={styles.bellWrap} style={vars} aria-label={`${count} new notifications`}>
<svg className={styles.bell} viewBox="0 0 256 256" width={40} height={40} aria-hidden="true">
<g fill="currentColor"><path d="M208 192H48a8 8 0 0 1-6.88-12C47.71 168.6 56 139.81 56 104a72 72 0 0 1 144 0c0 35.82 8.3 64.6 14.9 76a8 8 0 0 1-6.9 12" opacity=".2" /><path d="M221.8 175.94c-5.55-9.56-13.8-36.61-13.8-71.94a80 80 0 1 0-160 0c0 35.34-8.26 62.38-13.81 71.94A16 16 0 0 0 48 200h40.81a40 40 0 0 0 78.38 0H208a16 16 0 0 0 13.8-24.06M128 216a24 24 0 0 1-22.62-16h45.24A24 24 0 0 1 128 216m-80-32c7.7-13.24 16-43.92 16-80a64 64 0 1 1 128 0c0 36.05 8.28 66.73 16 80Z" /></g>
</svg>
<span className={styles.count}>{count}</span>
</div>
);
}06 · 通知カウントバッジ — 計測 累積面積 0.864% · 動いたフレーム 7/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
どこを変えるか
通知が届いた瞬間を伝えます。
.count {
transform: scale(0);
}
.stage.is-demo .bell {
animation: shake $duration $easing infinite;
}
.stage.is-demo .count {
animation: pop-in $duration $easing infinite;
}
@keyframes pop-in {
0%, 52% { transform: scale(0); }
62% { transform: scale(1.15); }
74%, 100% { transform: scale(1); }
}
07トレンディングフレイムバッジ
何が見えるか
炎形のSVGにfilter: brightnessとhue-rotateをフレームごとに微妙にずらしてかけています。

<main class="stage is-demo">
<span class="badge" aria-label="trending">
<svg class="flame" viewBox="0 0 256 256" width="26" height="26" aria-hidden="true">
<g fill="currentColor"><path d="M208 144a80 80 0 0 1-160 0c0-30.57 14.42-58.26 31-80l33 32l26.27-72C159.86 41.92 208 88.15 208 144" opacity=".2"/><path d="M183.89 153.34a57.6 57.6 0 0 1-46.56 46.55a9 9 0 0 1-1.33.11a8 8 0 0 1-1.32-15.89c16.57-2.79 30.63-16.85 33.44-33.45a8 8 0 0 1 15.78 2.68ZM216 144a88 88 0 0 1-176 0c0-27.92 11-56.47 32.66-84.85a8 8 0 0 1 11.93-.89l24.12 23.41l22-60.41a8 8 0 0 1 12.63-3.41C165.21 36 216 84.55 216 144m-16 0c0-46.09-35.79-85.92-58.21-106.33l-22.27 61.07a8 8 0 0 1-13.09 3L80.06 76.16C64.09 99.21 56 122 56 144a72 72 0 0 0 144 0"/></g>
</svg>
TRENDING
</span>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: 900ms; // 불꽃 한 번 일렁임 — 깜빡임은 빨라야 산다
$easing: $ease-out;
$color: $stage-orange;
@include stage($stage-yellow);
.badge {
display: inline-flex; align-items: center; gap: $sp-2;
padding: $sp-2 $sp-4;
border-radius: $r-pill;
background: $stage-orange;
color: $subject-ink;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-weight: 800;
font-size: 16px;
letter-spacing: .06em;
box-shadow: 0 0 $sp-2 rgba($color, .5);
}
.flame {
color: $stage-yellow; // Phosphor duotone: g fill="currentColor" 가 이 값을 물려받는다
filter: brightness(1) hue-rotate(0deg);
}
.stage.is-demo .flame {
animation: flicker $duration $easing infinite alternate;
}
.stage.is-demo .badge {
animation: glow-pulse #{$duration * 2} $easing infinite;
}
// 프레임마다 어긋나는 정지 구간 — 규칙적인 깜빡임이 아니라 진짜 불처럼
@keyframes flicker {
0% { filter: brightness(1) hue-rotate(0deg); transform: scale(1) skewX(0deg); }
18% { filter: brightness(1.25) hue-rotate(-6deg); transform: scale(1.05) skewX(-2deg); }
40% { filter: brightness(.92) hue-rotate(4deg); transform: scale(.97) skewX(1deg); }
63% { filter: brightness(1.15) hue-rotate(-3deg); transform: scale(1.03) skewX(-1deg); }
81% { filter: brightness(.97) hue-rotate(2deg); transform: scale(1) skewX(1deg); }
100% { filter: brightness(1.2) hue-rotate(-5deg); transform: scale(1.04) skewX(-2deg); }
}
@keyframes glow-pulse {
0%, 100% { box-shadow: 0 0 $sp-2 rgba($color, .5); }
50% { box-shadow: 0 0 $sp-6 rgba($color, .85), 0 0 $sp-2 rgba($stage-yellow, .5); }
}
@media (prefers-reduced-motion: reduce) {
.stage.is-demo .flame { animation: none; }
.stage.is-demo .badge { 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);
}
.badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border-radius: 999px;
background: #ff4d1f;
color: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-weight: 800;
font-size: 16px;
letter-spacing: 0.06em;
box-shadow: 0 0 8px rgba(255, 77, 31, 0.5);
}
.flame {
color: #ffd23f;
filter: brightness(1) hue-rotate(0deg);
}
.stage.is-demo .flame {
animation: flicker 900ms ease-out infinite alternate;
}
.stage.is-demo .badge {
animation: glow-pulse 1800ms ease-out infinite;
}
@keyframes flicker {
0% {
filter: brightness(1) hue-rotate(0deg);
transform: scale(1) skewX(0deg);
}
18% {
filter: brightness(1.25) hue-rotate(-6deg);
transform: scale(1.05) skewX(-2deg);
}
40% {
filter: brightness(0.92) hue-rotate(4deg);
transform: scale(0.97) skewX(1deg);
}
63% {
filter: brightness(1.15) hue-rotate(-3deg);
transform: scale(1.03) skewX(-1deg);
}
81% {
filter: brightness(0.97) hue-rotate(2deg);
transform: scale(1) skewX(1deg);
}
100% {
filter: brightness(1.2) hue-rotate(-5deg);
transform: scale(1.04) skewX(-2deg);
}
}
@keyframes glow-pulse {
0%, 100% {
box-shadow: 0 0 8px rgba(255, 77, 31, 0.5);
}
50% {
box-shadow: 0 0 24px rgba(255, 77, 31, 0.85), 0 0 8px rgba(255, 210, 63, 0.5);
}
}
@media (prefers-reduced-motion: reduce) {
.stage.is-demo .flame {
animation: none;
}
.stage.is-demo .badge {
animation: none;
}
}import type { CSSProperties } from "react";
import styles from "./BadgeTrendingFlame.module.scss";
export type BadgeTrendingFlameProps = {
duration?: number;
easing?: string;
color?: string;
label?: string;
};
export default function BadgeTrendingFlame({
duration = 900,
easing = "ease-out",
color = "#ff4d1f",
label = "TRENDING",
}: BadgeTrendingFlameProps) {
const vars: CSSProperties = { "--duration": `${duration}ms`, "--easing": easing, "--color": color } as CSSProperties;
return (
<span className={styles.badge} style={vars} aria-label="trending">
<svg className={styles.flame} viewBox="0 0 256 256" width={26} height={26} aria-hidden="true">
<g fill="currentColor"><path d="M208 144a80 80 0 0 1-160 0c0-30.57 14.42-58.26 31-80l33 32l26.27-72C159.86 41.92 208 88.15 208 144" opacity=".2" /><path d="M183.89 153.34a57.6 57.6 0 0 1-46.56 46.55a9 9 0 0 1-1.33.11a8 8 0 0 1-1.32-15.89c16.57-2.79 30.63-16.85 33.44-33.45a8 8 0 0 1 15.78 2.68ZM216 144a88 88 0 0 1-176 0c0-27.92 11-56.47 32.66-84.85a8 8 0 0 1 11.93-.89l24.12 23.41l22-60.41a8 8 0 0 1 12.63-3.41C165.21 36 216 84.55 216 144m-16 0c0-46.09-35.79-85.92-58.21-106.33l-22.27 61.07a8 8 0 0 1-13.09 3L80.06 76.16C64.09 99.21 56 122 56 144a72 72 0 0 0 144 0" /></g>
</svg>
{label}
</span>
);
}07 · トレンディングフレイムバッジ — 計測 累積面積 3.995% · 動いたフレーム 22/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
コードのどこを見るか
本物の炎のように規則的な点滅にはなりません。
.stage.is-demo .flame {
animation: flicker $duration $easing infinite alternate;
}
@keyframes flicker {
0% { filter: brightness(1) hue-rotate(0deg); transform: scale(1); }
18% { filter: brightness(1.25) hue-rotate(-6deg); transform: scale(1.05); }
40% { filter: brightness(.92) hue-rotate(4deg); transform: scale(.97); }
63% { filter: brightness(1.15) hue-rotate(-3deg); transform: scale(1.03); }
81% { filter: brightness(.97) hue-rotate(2deg); transform: scale(1); }
100% { filter: brightness(1.2) hue-rotate(-5deg); transform: scale(1.04); }
}
08在庫バーバッジ
画面で起きること
バッジ内の細いバーがtransform-origin: leftを基準にscaleXを1から0.25まで縮め、同時に背景色が緑→オレンジ→赤へと変わります。

<main class="stage is-demo">
<div class="badge" aria-label="3 left in stock">
<span class="label">3 LEFT</span>
<span class="track"><span class="fill"></span></span>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-loop; // 2s — 재고바가 줄어드는 한 주기
$easing: $ease-out;
$color: #50a26e; // 재고 넉넉함(시작 색)
@include stage($stage-paper);
.badge {
width: 148px;
padding: $sp-3 $sp-4;
border-radius: $r-card;
background: $subject-ink;
box-shadow: $shadow-raised;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.label {
display: block;
margin-bottom: $sp-2;
color: #fff;
font-weight: 700;
font-size: 15px;
letter-spacing: .04em;
}
.track {
display: block;
width: 100%; height: $sp-2;
border-radius: $r-xs;
background: rgba(255, 255, 255, .15);
overflow: hidden;
}
.fill {
display: block;
width: 100%; height: 100%;
border-radius: $r-xs;
background: $color;
transform: scaleX(1);
transform-origin: left; // 규칙8: 재고게이지 left 기준
}
.stage.is-demo .fill {
animation: drain $duration $easing infinite;
}
@keyframes drain {
0%, 10% { transform: scaleX(1); background: $color; }
50% { transform: scaleX(.6); background: $stage-orange; }
90%, 100% { transform: scaleX(.25); background: $error; }
}
@media (prefers-reduced-motion: reduce) {
.stage.is-demo .fill { animation: none; transform: scaleX(.25); background: $error; }
}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);
}
.badge {
width: 148px;
padding: 12px 16px;
border-radius: 16px;
background: #17141a;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.label {
display: block;
margin-bottom: 8px;
color: #fff;
font-weight: 700;
font-size: 15px;
letter-spacing: 0.04em;
}
.track {
display: block;
width: 100%;
height: 8px;
border-radius: 4px;
background: rgba(255, 255, 255, 0.15);
overflow: hidden;
}
.fill {
display: block;
width: 100%;
height: 100%;
border-radius: 4px;
background: #50a26e;
transform: scaleX(1);
transform-origin: left;
}
.stage.is-demo .fill {
animation: drain 2s ease-out infinite;
}
@keyframes drain {
0%, 10% {
transform: scaleX(1);
background: #50a26e;
}
50% {
transform: scaleX(0.6);
background: #ff4d1f;
}
90%, 100% {
transform: scaleX(0.25);
background: #c2361a;
}
}
@media (prefers-reduced-motion: reduce) {
.stage.is-demo .fill {
animation: none;
transform: scaleX(0.25);
background: #c2361a;
}
}import type { CSSProperties } from "react";
import styles from "./BadgeLimitedStock.module.scss";
export type BadgeLimitedStockProps = {
duration?: number;
easing?: string;
color?: string;
left?: number;
};
export default function BadgeLimitedStock({
duration = 2000,
easing = "ease-out",
color = "#16a34a",
left = 3,
}: BadgeLimitedStockProps) {
const vars: CSSProperties = { "--duration": `${duration}ms`, "--easing": easing, "--color": color } as CSSProperties;
return (
<div className={styles.badge} style={vars} aria-label={`${left} left in stock`}>
<span className={styles.label}>{left} LEFT</span>
<span className={styles.track}>
<span className={styles.fill} />
</span>
</div>
);
}08 · 在庫バーバッジ — 計測 累積面積 0.817% · 動いたフレーム 20/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
コードの読み方
長さと色の両方で在庫の減少を見せます。
.fill {
transform: scaleX(1);
transform-origin: left;
}
.stage.is-demo .fill {
animation: drain $duration $easing infinite;
}
@keyframes drain {
0%, 10% { transform: scaleX(1); background: $color; }
50% { transform: scaleX(.6); background: $stage-orange; }
90%, 100% { transform: scaleX(.25); background: $error; }
}
09完売ストライプオーバーレイ
どんな動きか
カード全体を覆うrepeating-linear-gradientの斜め縞模様が、background-positionの移動でカード全体を流れ続けます。

<main class="stage is-demo">
<div class="card" aria-label="sold out">
<div class="thumb"></div>
<div class="overlay">
<span class="stamp">SOLD OUT</span>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-loop; // 2s — 줄무늬 한 바퀴
$easing: linear;
$color: $stage-ink;
@include stage($stage-paper);
.card {
position: relative;
width: 220px; height: 140px;
border-radius: $r-card;
overflow: hidden;
box-shadow: $shadow-raised;
}
.thumb {
position: absolute; inset: 0;
background: linear-gradient(135deg, rgba($subject-blue, .25), rgba($subject-blue, .08));
}
.overlay {
position: absolute; inset: 0;
display: grid; place-items: center;
background:
repeating-linear-gradient(
-45deg,
rgba($color, .82) 0 $sp-4,
rgba($color, .62) $sp-4 #{$sp-4 * 2}
);
background-size: #{$sp-4 * 4} #{$sp-4 * 4};
}
.stamp {
padding: $sp-2 $sp-4;
border: 2px solid #fff;
border-radius: $r-sm;
color: #fff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-weight: 800;
font-size: 16px;
letter-spacing: .1em;
transform: scale(1) rotate(-6deg);
}
.stage.is-demo .overlay {
animation: stripes-flow $duration $easing infinite;
}
.stage.is-demo .stamp {
animation: stamp-breathe #{$duration} ease-in-out infinite;
}
@keyframes stripes-flow {
0% { background-position: 0 0; }
100% { background-position: #{$sp-4 * 4} 0; }
}
@keyframes stamp-breathe {
0%, 100% { transform: scale(1) rotate(-6deg); }
50% { transform: scale(1.08) rotate(-6deg); }
}
@media (prefers-reduced-motion: reduce) {
.stage.is-demo .overlay { animation: none; }
.stage.is-demo .stamp { 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);
}
.card {
position: relative;
width: 220px;
height: 140px;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
}
.thumb {
position: absolute;
inset: 0;
background: linear-gradient(135deg, rgba(47, 109, 246, 0.25), rgba(47, 109, 246, 0.08));
}
.overlay {
position: absolute;
inset: 0;
display: grid;
place-items: center;
background: repeating-linear-gradient(-45deg, rgba(23, 20, 26, 0.82) 0 16px, rgba(23, 20, 26, 0.62) 16px 32px);
background-size: 64px 64px;
}
.stamp {
padding: 8px 16px;
border: 2px solid #fff;
border-radius: 8px;
color: #fff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-weight: 800;
font-size: 16px;
letter-spacing: 0.1em;
transform: scale(1) rotate(-6deg);
}
.stage.is-demo .overlay {
animation: stripes-flow 2s linear infinite;
}
.stage.is-demo .stamp {
animation: stamp-breathe 2s ease-in-out infinite;
}
@keyframes stripes-flow {
0% {
background-position: 0 0;
}
100% {
background-position: 64px 0;
}
}
@keyframes stamp-breathe {
0%, 100% {
transform: scale(1) rotate(-6deg);
}
50% {
transform: scale(1.08) rotate(-6deg);
}
}
@media (prefers-reduced-motion: reduce) {
.stage.is-demo .overlay {
animation: none;
}
.stage.is-demo .stamp {
animation: none;
}
}import type { CSSProperties } from "react";
import styles from "./BadgeSoldOutOverlay.module.scss";
export type BadgeSoldOutOverlayProps = {
duration?: number;
easing?: string;
color?: string;
label?: string;
};
export default function BadgeSoldOutOverlay({
duration = 2000,
easing = "linear",
color = "#17141a",
label = "SOLD OUT",
}: BadgeSoldOutOverlayProps) {
const vars: CSSProperties = { "--duration": `${duration}ms`, "--easing": easing, "--color": color } as CSSProperties;
return (
<div className={styles.card} style={vars} aria-label="sold out">
<div className={styles.thumb} />
<div className={styles.overlay}>
<span className={styles.stamp}>{label}</span>
</div>
</div>
);
}09 · 完売ストライプオーバーレイ — 計測 累積面積 21.144% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
どこを変えるか
中央のSOLD OUTスタンプは呼吸するようにscaleが上下します。
.overlay {
background: repeating-linear-gradient(-45deg,
rgba($color, .82) 0 $sp-4,
rgba($color, .62) $sp-4 #{$sp-4 * 2});
background-size: #{$sp-4 * 4} #{$sp-4 * 4};
}
.stage.is-demo .overlay {
animation: stripes-flow $duration $easing infinite;
}
@keyframes stripes-flow {
0% { background-position: 0 0; }
100% { background-position: #{$sp-4 * 4} 0; }
}
どこで落とし穴にはまるか — リペイント頼みの動きは録画で消える
04番のベータバッジのグラデーションは、最初background-positionを動かして流していました。目で見れば確かに流れているのに、同じ画面をフレーム単位で比べるとフレーム間の差がほとんど出ません — background-positionのリペイントのタイミングはブラウザ任せで、キャプチャの瞬間とずれると変化が閾値の下に沈んでしまうからです。紫のレイヤーを1枚重ね、そのopacityだけを0から1へ上下させる方式に変えると、同じ見た目がフレームごとにはっきり残りました。背景位置や影のにじみのようにリペイントが要る属性ではなく、opacityやtransformのようにコンポジット段階で回る属性を選べば、目に見えるものと録画に残るものがずれません。色ではなく線の跡で画面を埋める側は手書き風 デザイン LPキットで出会えます。zipを解凍するときに聞かれるパスワードはwpxqsgtkで、表示されているとおりに入力すれば9つのフォルダがそのまま開きます。03番の認証チェックも同じ理由で、線を描き込む方式ではなくアイコン全体をtransform: scaleで弾ませています。
アクセシビリティ
9つすべてがprefers-reduced-motion: reduceのもとでanimation: noneになります。07番の炎の点滅や09番の縞模様オーバーレイのように動き続けるものも、静止した状態で色と形だけで意味が伝わるように止め値を決めてあります。03番のチェックはモーションをオフにすると元の大きさ(scale(1))のまま表示され、認証マーク自体が消えることはありません。この設定の仕組みはMDNのprefers-reduced-motionのページに詳しく載っています。
FAQ
画像ファイルなしで 通知 バッジ css は作れますか?
はい、9つすべて画像ファイルなしで、03・06・07番だけ短いインラインSVGアイコンを添えています。背景・影・clip-pathの形だけでリボンや値札の切り欠き、斜め縞模様まで表現できます。ほかのCSSだけの装飾はサイト内検索で「バッジ」と調べると見つかります。状態や優先度のように実務で使う向きの札はステータス バッジ UI 9選に別に集めています。
バッジの色を自分のブランドカラーに変えられますか?
変えられます。ほとんどのデモはSCSSの先頭にある$color変数ひとつを書き換えるだけで全体の色が変わり、React版も同じことをcolorpropひとつで行います。08番のように状態ごとに色が何段階も変わる項目は@keyframes内の各段階の色もあわせて書き換える必要があります(03番の認証マークはfill="currentColor"なのでcolorの値をそのまま受け継ぎます)。
バッジのアニメーションが画面収録で薄くしか映らないのですが?
動かす属性を変えてください。背景位置や影のにじみはリペイントが要るためブラウザ任せのタイミングで描かれ、キャプチャとずれやすい一方、opacityとtransformはコンポジット段階で回るのでフレームごとにそのまま残ります。このサイトを初めて見た方は、紹介ページでデモをどう作って検証しているかを確認できます。









