05Discount pricing
연간 결제 스위치를 누르면 aria-checked가 실제로 바뀌면서 월간·연간 가격판이 opacity로 크로스페이드되고, 원가에는 취소선이 그대로 남습니다.
Best for
role=switchopacityclick

<main class="stage">
<section class="dp">
<span class="dp__badge">Launch discount</span>
<div class="dp__switch-row">
<span class="dp__switch-label">Billed yearly</span>
<button type="button" class="dp__switch" role="switch" aria-checked="false"><span class="dp__knob"></span></button>
</div>
<div class="dp__price-wrap">
<div class="dp__price dp__price--monthly is-active">
<span class="dp__old">₩19,000</span>
<span class="dp__now">₩12,900<small>/mo</small></span>
</div>
<div class="dp__price dp__price--yearly">
<span class="dp__old">₩228,000</span>
<span class="dp__now">₩9,900<small>/mo</small></span>
</div>
</div>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-base; // 가격판 크로스페이드
$easing: $ease-spring;
$color: $subject-blue;
@include stage($stage-ink);
.dp { position: relative; width: min(300px, 94vw); text-align: center; padding-top: $sp-5; }
.dp__badge {
position: absolute;
top: 0; left: 50%;
transform: translateX(-50%);
padding: $sp-1 $sp-3;
border-radius: $r-pill;
background: rgba(255, 210, 63, .2);
color: $stage-yellow;
font-weight: 700; font-size: 10px; line-height: 1; font-family: inherit;
letter-spacing: .04em;
animation: dp-badge 2.2s ease-in-out infinite;
}
@keyframes dp-badge {
0%, 100% { transform: translateX(-50%) scale(1); }
50% { transform: translateX(-50%) scale(1.08); }
}
.dp__switch-row { display: flex; align-items: center; justify-content: center; gap: $sp-2; margin: $sp-4 0; }
.dp__switch-label { font-weight: 600; font-size: 12px; line-height: 1; font-family: inherit; color: rgba(255, 247, 230, .7); }
.dp__switch {
position: relative;
width: 40px; height: 22px;
border: 0;
border-radius: $r-pill;
background: rgba(255, 247, 230, .18);
cursor: pointer;
transition: background $duration $easing;
}
.dp__switch[aria-checked="true"] { background: $subject-blue; }
.dp__knob {
position: absolute;
top: 3px; left: 3px;
width: 16px; height: 16px;
border-radius: 50%;
background: $subject-cream;
transition: transform $duration $easing;
}
.dp__switch[aria-checked="true"] .dp__knob { transform: translateX(18px); }
.dp__price-wrap { position: relative; height: 64px; }
.dp__price {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-1;
opacity: 0;
transition: opacity $duration $easing;
}
.dp__price.is-active { opacity: 1; }
.dp__old { font-weight: 600; font-size: 12px; line-height: 1; font-family: inherit; color: rgba(255, 247, 230, .4); text-decoration: line-through; }
.dp__now { font-weight: 800; font-size: clamp(22px, 7vw, 28px); line-height: 1; font-family: inherit; color: $subject-cream; }
.dp__now small { font-weight: 600; font-size: 11px; line-height: 1; font-family: inherit; color: rgba(255, 247, 230, .55); }
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.dp__badge { animation: none !important; }
.dp__switch, .dp__knob, .dp__price { transition: none !important; }
}
// ── 반응형(320×200) ──────────────────────────────────
@media (max-width: 340px), (max-height: 220px) {
.dp { padding-top: $sp-4; }
.dp__switch-row { margin: $sp-3 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);
}
.dp {
position: relative;
width: min(300px, 94vw);
text-align: center;
padding-top: 20px;
}
.dp__badge {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
padding: 4px 12px;
border-radius: 999px;
background: rgba(255, 210, 63, 0.2);
color: #ffd23f;
font-weight: 700;
font-size: 10px;
line-height: 1;
font-family: inherit;
letter-spacing: 0.04em;
animation: dp-badge 2.2s ease-in-out infinite;
}
@keyframes dp-badge {
0%, 100% {
transform: translateX(-50%) scale(1);
}
50% {
transform: translateX(-50%) scale(1.08);
}
}
.dp__switch-row {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin: 16px 0;
}
.dp__switch-label {
font-weight: 600;
font-size: 12px;
line-height: 1;
font-family: inherit;
color: rgba(255, 247, 230, 0.7);
}
.dp__switch {
position: relative;
width: 40px;
height: 22px;
border: 0;
border-radius: 999px;
background: rgba(255, 247, 230, 0.18);
cursor: pointer;
transition: background 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.dp__switch[aria-checked=true] {
background: #2f6df6;
}
.dp__knob {
position: absolute;
top: 3px;
left: 3px;
width: 16px;
height: 16px;
border-radius: 50%;
background: #fff7e6;
transition: transform 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.dp__switch[aria-checked=true] .dp__knob {
transform: translateX(18px);
}
.dp__price-wrap {
position: relative;
height: 64px;
}
.dp__price {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
opacity: 0;
transition: opacity 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.dp__price.is-active {
opacity: 1;
}
.dp__old {
font-weight: 600;
font-size: 12px;
line-height: 1;
font-family: inherit;
color: rgba(255, 247, 230, 0.4);
text-decoration: line-through;
}
.dp__now {
font-weight: 800;
font-size: clamp(22px, 7vw, 28px);
line-height: 1;
font-family: inherit;
color: #fff7e6;
}
.dp__now small {
font-weight: 600;
font-size: 11px;
line-height: 1;
font-family: inherit;
color: rgba(255, 247, 230, 0.55);
}
@media (prefers-reduced-motion: reduce) {
.dp__badge {
animation: none !important;
}
.dp__switch, .dp__knob, .dp__price {
transition: none !important;
}
}
@media (max-width: 340px), (max-height: 220px) {
.dp {
padding-top: 16px;
}
.dp__switch-row {
margin: 12px 0;
}
}// 실제 동작: 스위치를 누르면 aria-checked 가 실제로 바뀌고 월간/연간 가격판이 opacity 로 크로스페이드된다.
var sw = document.querySelector('.dp__switch');
var monthly = document.querySelector('.dp__price--monthly');
var yearly = document.querySelector('.dp__price--yearly');
sw.addEventListener('click', function () {
var on = sw.getAttribute('aria-checked') === 'true';
sw.setAttribute('aria-checked', String(!on));
monthly.classList.toggle('is-active', on);
yearly.classList.toggle('is-active', !on);
});import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./DiscountPricing.module.scss";
export type DiscountPricingProps = { duration?: number; easing?: string; color?: string };
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function DiscountPricing({ duration = 300, easing = "cubic-bezier(.2,.8,.2,1)", color = "#2f6df6" }: DiscountPricingProps) {
const [yearly, setYearly] = useState(false);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.dp} style={vars}>
<span className={styles.badge}>런칭 할인</span>
<div className={styles.switchRow}>
<span className={styles.switchLabel}>연간 결제</span>
<button type="button" className={styles.switch} role="switch" aria-checked={yearly} onClick={() => setYearly((v) => !v)}>
<span className={styles.knob} />
</button>
</div>
<div className={styles.priceWrap}>
<div className={`${styles.price} ${!yearly ? styles.active : ""}`}>
<span className={styles.old}>₩19,000</span>
<span className={styles.now}>₩12,900<small>/월</small></span>
</div>
<div className={`${styles.price} ${yearly ? styles.active : ""}`}>
<span className={styles.old}>₩228,000</span>
<span className={styles.now}>₩9,900<small>/월</small></span>
</div>
</div>
</section>
);
}05 Discount pricing — Measured area 0.458% · moving frames 17/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


