05알림 점 더블 펄스
213편의 라이브 뱃지처럼 링 하나가 계속 퍼지는 게 아니라, 아이콘 코너의 점이 scale(1→1.35→1)로 두 번 빠르게 튀고 그 사이마다 옅은 링이 짧게 번쩍인 뒤 남은 시간은 완전히 멈춰, 새 메시지 두 개가 막 도착한 리듬을 만든다.
쓰임채팅 아이콘 미확인 표시, 내비게이션 바 새 메시지 신호
scale 더블 스프링@keyframes double-ring무대 ink

<main class="stage is-demo">
<div class="icon-wrap" aria-label="새 메시지 있음">
<svg class="icon" viewBox="0 0 256 256" width="40" height="40" aria-hidden="true"><g fill="currentColor"><path d="M224,128A96,96,0,0,1,79.93,211.11h0L42.54,223.58a8,8,0,0,1-10.12-10.12l12.47-37.39h0A96,96,0,1,1,224,128Z" opacity=".2"/><path d="M128,24A104,104,0,0,0,36.18,176.88L24.83,210.93a16,16,0,0,0,20.24,20.24l34.05-11.35A104,104,0,1,0,128,24Zm0,192a87.87,87.87,0,0,1-44.06-11.81,8,8,0,0,0-6.54-.67L40,216,52.47,178.6a8,8,0,0,0-.66-6.54A88,88,0,1,1,128,216Z"/></g></svg>
<span class="dot" aria-hidden="true">
<span class="dot__ring"></span>
</span>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-loop; // 2s — 더블 펄스 + 휴지 한 주기
$easing: $ease-pop;
$color: $error;
@include stage($stage-ink);
.icon-wrap {
position: relative;
width: 64px; height: 64px;
display: grid; place-items: center;
}
.icon { color: $stage-yellow; }
.dot {
position: absolute;
top: 10px; right: 10px;
width: 10px; height: 10px;
border-radius: 50%;
background: $color;
box-shadow: 0 0 0 2px $stage-ink;
}
.dot__ring {
position: absolute; inset: 0;
border-radius: 50%;
border: 2px solid $color;
opacity: 0;
transform: scale(1);
}
// 아이콘은 그대로 — 점과 링만 두 번 튀듯 움직인다 (213의 단일 레이더 핑과 다른 리듬)
.stage.is-demo .dot {
animation: double-pulse $duration $easing infinite;
}
.stage.is-demo .dot__ring {
animation: double-ring $duration $easing infinite;
}
@keyframes double-pulse {
0%, 100% { transform: scale(1); }
8%, 24% { transform: scale(1.35); }
16% { transform: scale(1); }
32% { transform: scale(1); }
}
@keyframes double-ring {
0%, 6% { opacity: 0; transform: scale(1); }
20% { opacity: .5; transform: scale(2.4); }
34% { opacity: 0; transform: scale(2.6); }
100% { opacity: 0; transform: scale(1); }
}
// ── 한 겹: 두 번째 펄스(24%) 순간 도트 색 전이 + 세 번째 물결 링 ──
// double-pulse/double-ring 은 그대로 — dot 배경색만 잠깐 밝아지고, 물결 링은 두 링을 뒤따라 확산
.icon-wrap::after {
content: "";
position: absolute;
top: 5px; right: 5px; // dot(10px) 중심 — inset -3px 원형 링
width: 16px; height: 16px;
border-radius: 50%;
border: 2px solid rgba($color, 0);
opacity: 0;
pointer-events: none;
}
.stage.is-demo .dot {
animation: double-pulse $duration $easing infinite, dot-tint $duration $ease-out infinite;
}
.stage.is-demo .icon-wrap::after {
animation: wake-ripple $duration $easing infinite;
}
@keyframes dot-tint {
0%, 18% { background-color: $color; }
24% { background-color: mix(#fff, $color, 35%); }
38%, 100% { background-color: $color; }
}
@keyframes wake-ripple {
0%, 18% { opacity: 0; transform: scale(.9); border-color: rgba($color, 0); }
26% { opacity: .7; transform: scale(1); border-color: rgba(mix(#ffd9c9, $color, 35%), .85); }
52%, 100% { opacity: 0; transform: scale(2.2); border-color: rgba($color, 0); }
}
@media (prefers-reduced-motion: reduce) {
.stage.is-demo .dot { animation: none; }
.stage.is-demo .dot__ring { animation: none; opacity: 0; }
.stage.is-demo .icon-wrap::after { 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);
}
.icon-wrap {
position: relative;
width: 64px;
height: 64px;
display: grid;
place-items: center;
}
.icon {
color: #ffd23f;
}
.dot {
position: absolute;
top: 10px;
right: 10px;
width: 10px;
height: 10px;
border-radius: 50%;
background: #c2361a;
box-shadow: 0 0 0 2px #17141a;
}
.dot__ring {
position: absolute;
inset: 0;
border-radius: 50%;
border: 2px solid #c2361a;
opacity: 0;
transform: scale(1);
}
.stage.is-demo .dot {
animation: double-pulse 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
.stage.is-demo .dot__ring {
animation: double-ring 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
@keyframes double-pulse {
0%, 100% {
transform: scale(1);
}
8%, 24% {
transform: scale(1.35);
}
16% {
transform: scale(1);
}
32% {
transform: scale(1);
}
}
@keyframes double-ring {
0%, 6% {
opacity: 0;
transform: scale(1);
}
20% {
opacity: 0.5;
transform: scale(2.4);
}
34% {
opacity: 0;
transform: scale(2.6);
}
100% {
opacity: 0;
transform: scale(1);
}
}
.icon-wrap::after {
content: "";
position: absolute;
top: 5px;
right: 5px;
width: 16px;
height: 16px;
border-radius: 50%;
border: 2px solid rgba(194, 54, 26, 0);
opacity: 0;
pointer-events: none;
}
.stage.is-demo .dot {
animation: double-pulse 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite, dot-tint 2s ease-out infinite;
}
.stage.is-demo .icon-wrap::after {
animation: wake-ripple 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
@keyframes dot-tint {
0%, 18% {
background-color: #c2361a;
}
24% {
background-color: rgb(84.4509803922%, 48.7647058824%, 41.6274509804%);
}
38%, 100% {
background-color: #c2361a;
}
}
@keyframes wake-ripple {
0%, 18% {
opacity: 0;
transform: scale(0.9);
border-color: rgba(194, 54, 26, 0);
}
26% {
opacity: 0.7;
transform: scale(1);
border-color: rgba(84.4509803922%, 43.5490196078%, 34.2156862745%, 0.85);
}
52%, 100% {
opacity: 0;
transform: scale(2.2);
border-color: rgba(194, 54, 26, 0);
}
}
@media (prefers-reduced-motion: reduce) {
.stage.is-demo .dot {
animation: none;
}
.stage.is-demo .dot__ring {
animation: none;
opacity: 0;
}
.stage.is-demo .icon-wrap::after {
animation: none;
opacity: 0;
}
}import type { CSSProperties } from "react";
import styles from "./NotificationDotPulse.module.scss";
export type NotificationDotPulseProps = {
duration?: number;
easing?: string;
color?: string;
};
export default function NotificationDotPulse({
duration = 2000,
easing = "cubic-bezier(.2,1.4,.4,1)",
color = "#c2361a",
}: NotificationDotPulseProps) {
const vars: CSSProperties = { "--duration": `${duration}ms`, "--easing": easing, "--color": color } as CSSProperties;
return (
<div className={styles.wrap} style={vars} aria-label="new message">
<svg className={styles.icon} viewBox="0 0 256 256" width={40} height={40} aria-hidden="true">
<g fill="currentColor">
<path d="M224,128A96,96,0,0,1,79.93,211.11h0L42.54,223.58a8,8,0,0,1-10.12-10.12l12.47-37.39h0A96,96,0,1,1,224,128Z" opacity=".2" />
<path d="M128,24A104,104,0,0,0,36.18,176.88L24.83,210.93a16,16,0,0,0,20.24,20.24l34.05-11.35A104,104,0,1,0,128,24Zm0,192a87.87,87.87,0,0,1-44.06-11.81,8,8,0,0,0-6.54-.67L40,216,52.47,178.6a8,8,0,0,0-.66-6.54A88,88,0,1,1,128,216Z" />
</g>
</svg>
<span className={styles.dot}>
<span className={styles.ring} />
</span>
</div>
);
}05 알림 점 더블 펄스 — 측정 누적면적 0.648% · 움직인 프레임 9/23GODRICH 원작 · zip 에 SCSS·React 포함 · 태그 UI 디자인 9종 — 상태·우선순위·인증 배지 코드
이 데모가 속한 글
태그 UI 디자인 9종 — 상태·우선순위·인증 배지 코드태그 UI 디자인은 상태·카테고리·우선순위 같은 정보를 작은 알약이나 칩 모양으로 압축해 보여주는 장식이다. 온라인 점 하나, 우선순위 색 하나만 봐도 상태를 바로 알 수 있고, 움직임을 더하면 그대로 붙여서 훨씬 이 글의 데모 전부 보기


