06Urgency banner
마감 임박 진행바가 계속 은은하게 출렁이는 동안, '자세히' 버튼을 누르면 aria-pressed가 바뀌면서 마감 안내 문구가 실제로 늘어납니다.
Best for
scaleXclickaria-pressed

<main class="stage">
<section class="ub">
<span class="ub__live" aria-hidden="true"><span class="ub__dot"></span>LIVE</span>
<div class="ub__body">
<p class="ub__msg">Only <strong>5 spots</strong> left</p>
<div class="ub__bar" role="progressbar" aria-valuenow="82" aria-valuemin="0" aria-valuemax="100" aria-label="Spots filled">
<span class="ub__fill"></span>
</div>
</div>
<button type="button" class="ub__more" aria-pressed="false">Details</button>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-quick; // "자세히" 전환
$easing: $ease-pop;
$color: $error;
@include stage($stage-yellow);
.ub {
box-sizing: border-box; // width 에 패딩을 포함 — 아니면 320px 에서 패딩만큼 넘친다(실측 2026-09-10)
width: min(400px, 96vw);
display: flex;
align-items: center;
gap: $sp-3;
padding: $sp-3 $sp-4;
border-radius: $r-control;
background: $subject-ink;
box-shadow: $shadow-raised;
}
.ub__live {
display: flex;
align-items: center;
gap: $sp-1;
flex: 0 0 auto;
padding: $sp-1 $sp-2;
border-radius: $r-xs;
background: rgba(194, 54, 26, .22);
color: #e0c8c1;
font-weight: 800; font-size: 9px; line-height: 1; font-family: inherit;
letter-spacing: .06em;
}
.ub__dot { width: 6px; height: 6px; border-radius: 50%; background: #d1a9a1; animation: ub-blink 1.2s ease-in-out infinite; }
@keyframes ub-blink {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: .35; transform: scale(.8); }
}
.ub__body { flex: 1 1 auto; min-width: 0; }
.ub__msg { margin: 0 0 $sp-1; font-weight: 600; font-size: 12px; line-height: 1.3; font-family: inherit; color: $subject-cream; }
.ub__msg strong { color: $stage-yellow; }
.ub__bar { height: 6px; border-radius: $r-xs; background: rgba(255, 247, 230, .14); overflow: hidden; }
.ub__fill {
display: block;
height: 100%;
width: 100%;
border-radius: $r-xs;
background: linear-gradient(90deg, #dcc2b7, #d1a9a1);
transform: scaleX(.82);
transform-origin: left center; // 규칙8: 재고 막대 왼쪽 채움
animation: ub-fill 2.4s $easing infinite;
}
@keyframes ub-fill {
0%, 100% { transform: scaleX(.78); }
50% { transform: scaleX(.92); }
}
.ub__more {
flex: 0 0 auto;
border: 0;
cursor: pointer;
padding: $sp-1 $sp-3;
border-radius: $r-pill;
background: rgba(255, 247, 230, .12);
color: $subject-cream;
font-weight: 700; font-size: 11px; line-height: 1; font-family: inherit;
transition: background $duration $easing;
}
.ub__more[aria-pressed="true"] { background: rgba(255, 247, 230, .28); }
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.ub__dot, .ub__fill { animation: none !important; }
.ub__more { transition: none !important; }
}
// ── 반응형(320×200) ──────────────────────────────────
@media (max-width: 340px), (max-height: 220px) {
.ub { flex-wrap: wrap; padding: $sp-2 $sp-3; }
.ub__more { display: 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);
}
.ub {
box-sizing: border-box;
width: min(400px, 96vw);
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
border-radius: 12px;
background: #17141a;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
}
.ub__live {
display: flex;
align-items: center;
gap: 4px;
flex: 0 0 auto;
padding: 4px 8px;
border-radius: 4px;
background: rgba(194, 54, 26, 0.22);
color: #e0c8c1;
font-weight: 800;
font-size: 9px;
line-height: 1;
font-family: inherit;
letter-spacing: 0.06em;
}
.ub__dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #d1a9a1;
animation: ub-blink 1.2s ease-in-out infinite;
}
@keyframes ub-blink {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.35;
transform: scale(0.8);
}
}
.ub__body {
flex: 1 1 auto;
min-width: 0;
}
.ub__msg {
margin: 0 0 4px;
font-weight: 600;
font-size: 12px;
line-height: 1.3;
font-family: inherit;
color: #fff7e6;
}
.ub__msg strong {
color: #ffd23f;
}
.ub__bar {
height: 6px;
border-radius: 4px;
background: rgba(255, 247, 230, 0.14);
overflow: hidden;
}
.ub__fill {
display: block;
height: 100%;
width: 100%;
border-radius: 4px;
background: linear-gradient(90deg, #dcc2b7, #d1a9a1);
transform: scaleX(0.82);
transform-origin: left center;
animation: ub-fill 2.4s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
@keyframes ub-fill {
0%, 100% {
transform: scaleX(0.78);
}
50% {
transform: scaleX(0.92);
}
}
.ub__more {
flex: 0 0 auto;
border: 0;
cursor: pointer;
padding: 4px 12px;
border-radius: 999px;
background: rgba(255, 247, 230, 0.12);
color: #fff7e6;
font-weight: 700;
font-size: 11px;
line-height: 1;
font-family: inherit;
transition: background 220ms cubic-bezier(0.2, 1.4, 0.4, 1);
}
.ub__more[aria-pressed=true] {
background: rgba(255, 247, 230, 0.28);
}
@media (prefers-reduced-motion: reduce) {
.ub__dot, .ub__fill {
animation: none !important;
}
.ub__more {
transition: none !important;
}
}
@media (max-width: 340px), (max-height: 220px) {
.ub {
flex-wrap: wrap;
padding: 8px 12px;
}
.ub__more {
display: none;
}
}// 실제 동작: "Details" 를 누르면 aria-pressed 가 바뀌고 알림 문구가 실제로 펼쳐진다.
var btn = document.querySelector('.ub__more');
var msg = document.querySelector('.ub__msg');
var base = msg.textContent;
btn.addEventListener('click', function () {
var on = btn.getAttribute('aria-pressed') === 'true';
btn.setAttribute('aria-pressed', String(!on));
msg.innerHTML = on ? 'Only <strong>5 spots</strong> left' : 'Only <strong>5 spots</strong> left · full price after the deadline';
});import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./UrgencyBanner.module.scss";
export type UrgencyBannerProps = { duration?: number; easing?: string; color?: string; spotsLeft?: number };
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function UrgencyBanner({ duration = 220, easing = "cubic-bezier(.2,1.4,.4,1)", color = "#c2361a", spotsLeft = 5 }: UrgencyBannerProps) {
const [expanded, setExpanded] = useState(false);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.ub} style={vars}>
<span className={styles.live} aria-hidden="true"><span className={styles.dot} />LIVE</span>
<div className={styles.body}>
<p className={styles.msg}>
선착순 마감까지 <strong>{spotsLeft}자리</strong> 남았어요{expanded ? " · 마감 후 정가 복귀" : ""}
</p>
<div className={styles.bar} role="progressbar" aria-valuenow={82} aria-valuemin={0} aria-valuemax={100} aria-label="선착순 마감률">
<span className={styles.fill} />
</div>
</div>
<button type="button" className={styles.more} aria-pressed={expanded} onClick={() => setExpanded((v) => !v)}>
자세히
</button>
</div>
);
}06 Urgency banner — Measured area 0.192% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · 9 Promotion Landing Page Templates — Copy-Paste
The post this demo lives in
9 Promotion Landing Page Templates — Copy-PasteA promotion landing page template packages the sections a campaign launch runs on — countdown, proof, pricing, sign-up — See all demos in this post


