06알림 카운트 뱃지
아이콘 위 작은 원이 scale(0)에서 튕기듯 1.15 를 지나 1 로 안착하고, 그 순간 아이콘 자체도 짧게 rotate 로 흔들려 새 알림이 막 도착했다는 신호를 준다.
쓰임알림함 벨 아이콘, 장바구니·메시지 미확인 개수 표시
@keyframes scale bouncetransform-origin@media (prefers-reduced-motion)

<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 원작 · zip 에 SCSS·React 포함 · 뱃지 디자인 UI 9종 — 신상·세일·인증 배지 코드
이 데모가 속한 글
뱃지 디자인 UI 9종 — 신상·세일·인증 배지 코드뱃지 디자인 UI는 상품 썸네일이나 이름 옆에 작게 붙어 지금 상태를 알려주는 라벨 장식이다. NEW·세일·인증·품절처럼 사진 없이 코드 몇 줄로 만들 수 있고, 움직임 하나만 더하면 복붙 몇 줄로도 훨씬 눈에 띈다 이 글의 데모 전부 보기


