06마감 임박 배너
마감 임박 진행바가 계속 은은하게 출렁이는 동안, '자세히' 버튼을 누르면 aria-pressed가 바뀌면서 마감 안내 문구가 실제로 늘어납니다.
쓰임프로모션 랜딩 상단 고정 마감 임박 알림 배너
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">선착순 마감까지 <strong>5자리</strong> 남았어요</p>
<div class="ub__bar" role="progressbar" aria-valuenow="82" aria-valuemin="0" aria-valuemax="100" aria-label="선착순 마감률">
<span class="ub__fill"></span>
</div>
</div>
<button type="button" class="ub__more" aria-pressed="false">자세히</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;
}
}// 실제 동작: "자세히" 를 누르면 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 ? '선착순 마감까지 <strong>5자리</strong> 남았어요' : '선착순 마감까지 <strong>5자리</strong> 남았어요 · 마감 후 정가 복귀';
});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 마감 임박 배너 — 측정 누적면적 0.192% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 프로모션 페이지 랜딩 섹션 템플릿 9종
이 데모가 속한 글
프로모션 페이지 랜딩 섹션 템플릿 9종프로모션 페이지 섹션 템플릿은 카운트다운 히어로부터 신청 폼까지 캠페인 랜딩에 필요한 구성 요소를 통째로 담은 코드 모음입니다. 랜딩 페이지에 바로 붙이는 아홉 개 중 하나는 실제 목표 시각까지 매초 줄어드는 카운트 이 글의 데모 전부 보기


