03Pricing cards — a list where only the recommended card gets a larger scale value
The recommended plan's scale value is computed larger than its neighbors so it appears to float, and once the billing cycle changes, the shown price recalculates to that value.
Best forA pricing comparison section, a SaaS plan overview
classList.togglenth-child(2)steps(1, end)

<main class="stage" id="stage">
<div class="pricing is-demo" id="pricing">
<div class="toggle" role="group" aria-label="Billing cycle">
<button class="toggle__btn toggle__btn--m is-active" id="btn-monthly" type="button" aria-pressed="true">Monthly</button>
<button class="toggle__btn toggle__btn--y" id="btn-yearly" type="button" aria-pressed="false">Yearly -20%</button>
</div>
<div class="plans">
<article class="plan">
<p class="plan__name">Starter</p>
<div class="plan__price">
<span class="plan__amount plan__amount--m">9,000</span>
<span class="plan__amount plan__amount--y">7,200</span>
</div>
<div class="plan__unit-row">
<span class="plan__unit plan__unit--m">/mo</span>
<span class="plan__unit plan__unit--y">/mo (billed yearly)</span>
</div>
</article>
<article class="plan plan--featured">
<p class="plan__badge">Recommended</p>
<p class="plan__name">Pro</p>
<div class="plan__price">
<span class="plan__amount plan__amount--m">19,000</span>
<span class="plan__amount plan__amount--y">15,200</span>
</div>
<div class="plan__unit-row">
<span class="plan__unit plan__unit--m">/mo</span>
<span class="plan__unit plan__unit--y">/mo (billed yearly)</span>
</div>
</article>
<article class="plan">
<p class="plan__name">Business</p>
<div class="plan__price">
<span class="plan__amount plan__amount--m">39,000</span>
<span class="plan__amount plan__amount--y">31,200</span>
</div>
<div class="plan__unit-row">
<span class="plan__unit plan__unit--m">/mo</span>
<span class="plan__unit plan__unit--y">/mo (billed yearly)</span>
</div>
</article>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 계산 상수: 추천 카드와 주변 카드의 scale 값 차이
$featured-scale: 1.08;
$duration: $dur-loop;
$easing: steps(1, end);
$color: $subject-cream;
@include stage($stage-orange);
.pricing {
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-4;
width: 100%;
}
.toggle {
display: inline-flex;
gap: $sp-1;
padding: $sp-1;
border-radius: $r-pill;
background: rgba($subject-ink, .18);
}
.toggle__btn {
border: 0;
border-radius: $r-pill;
padding: $sp-1 $sp-4;
font-family: inherit;
font-weight: 600;
font-size: 11px;
line-height: 1.8;
cursor: pointer;
transition: background $dur-base $ease-out, color $dur-base $ease-out;
}
.toggle__btn.is-active {
background: $color;
color: $subject-orange-deep;
}
.toggle__btn:not(.is-active) {
background: transparent;
color: $color;
}
.plans {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: $sp-3;
width: 100%;
max-width: 420px;
}
.plan {
position: relative;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-1;
padding: $sp-3 $sp-2;
border-radius: $r-card;
background: $stage-paper;
box-shadow: $shadow-press;
}
.plan--featured {
z-index: 1;
background: $subject-ink;
transform: scale(#{$featured-scale});
}
.is-demo .plan--featured {
animation: featured-scale-in $dur-enter $ease-spring both;
}
@keyframes featured-scale-in {
0% { transform: scale(1); }
100% { transform: scale(#{$featured-scale}); }
}
.plan__badge {
margin: 0;
padding: $sp-1 $sp-2;
border-radius: $r-xs;
background: $color;
color: $subject-orange-deep;
font-weight: 700;
font-size: 9px;
line-height: 1.4;
}
.plan__name {
margin: 0;
color: $subject-ink;
font-weight: 600;
font-size: 12px;
line-height: 1.4;
}
.plan--featured .plan__name { color: $stage-paper; }
.plan__price {
position: relative;
margin: 0;
min-height: 20px;
width: 100%;
text-align: center;
}
.plan__amount {
position: absolute;
inset: 0;
color: $subject-ink;
font-weight: 700;
font-size: 15px;
line-height: 1.4;
white-space: nowrap;
opacity: 0;
transition: opacity $dur-base $ease-out;
}
.plan--featured .plan__amount { color: $stage-paper; }
.plan__amount--m { opacity: 1; }
.plan__unit-row {
position: relative;
width: 100%;
height: 13px;
text-align: center;
}
.plan__unit {
position: absolute;
inset: 0;
color: rgba($subject-ink, .72);
font-weight: 400;
font-size: 9px;
line-height: 1.4;
white-space: nowrap;
opacity: 0;
transition: opacity $dur-base $ease-out;
}
.plan--featured .plan__unit { color: rgba($stage-paper, .78); }
.plan__unit--m { opacity: 1; }
.pricing.is-yearly .plan__amount--m,
.pricing.is-yearly .plan__unit--m { opacity: 0; }
.pricing.is-yearly .plan__amount--y,
.pricing.is-yearly .plan__unit--y { opacity: 1; }
.is-demo .plans > .plan:nth-child(1) .plan__amount--m,
.is-demo .plans > .plan:nth-child(1) .plan__unit--m {
animation: fade-m-1 $duration $easing infinite;
}
.is-demo .plans > .plan:nth-child(1) .plan__amount--y,
.is-demo .plans > .plan:nth-child(1) .plan__unit--y {
animation: fade-y-1 $duration $easing infinite;
}
.is-demo .plans > .plan:nth-child(2) .plan__amount--m,
.is-demo .plans > .plan:nth-child(2) .plan__unit--m {
animation: fade-m-2 $duration $easing infinite;
}
.is-demo .plans > .plan:nth-child(2) .plan__amount--y,
.is-demo .plans > .plan:nth-child(2) .plan__unit--y {
animation: fade-y-2 $duration $easing infinite;
}
.is-demo .plans > .plan:nth-child(3) .plan__amount--m,
.is-demo .plans > .plan:nth-child(3) .plan__unit--m {
animation: fade-m-3 $duration $easing infinite;
}
.is-demo .plans > .plan:nth-child(3) .plan__amount--y,
.is-demo .plans > .plan:nth-child(3) .plan__unit--y {
animation: fade-y-3 $duration $easing infinite;
}
@keyframes fade-m-1 { 0%, 25% { opacity: 1; } 30%, 100% { opacity: 0; } }
@keyframes fade-y-1 { 0%, 25% { opacity: 0; } 30%, 100% { opacity: 1; } }
@keyframes fade-m-2 { 0%, 45% { opacity: 1; } 50%, 100% { opacity: 0; } }
@keyframes fade-y-2 { 0%, 45% { opacity: 0; } 50%, 100% { opacity: 1; } }
@keyframes fade-m-3 { 0%, 65% { opacity: 1; } 70%, 100% { opacity: 0; } }
@keyframes fade-y-3 { 0%, 65% { opacity: 0; } 70%, 100% { opacity: 1; } }
@media (max-width: 340px) {
.plans { gap: $sp-1; max-width: 300px; }
.plan { padding: $sp-2 $sp-1; }
.plan__amount { font-size: 12px; }
}
@media (prefers-reduced-motion: reduce) {
.is-demo .plans > .plan:nth-child(1) .plan__amount--m,
.is-demo .plans > .plan:nth-child(1) .plan__amount--y,
.is-demo .plans > .plan:nth-child(1) .plan__unit--m,
.is-demo .plans > .plan:nth-child(1) .plan__unit--y,
.is-demo .plans > .plan:nth-child(2) .plan__amount--m,
.is-demo .plans > .plan:nth-child(2) .plan__amount--y,
.is-demo .plans > .plan:nth-child(2) .plan__unit--m,
.is-demo .plans > .plan:nth-child(2) .plan__unit--y,
.is-demo .plans > .plan:nth-child(3) .plan__amount--m,
.is-demo .plans > .plan:nth-child(3) .plan__amount--y,
.is-demo .plans > .plan:nth-child(3) .plan__unit--m,
.is-demo .plans > .plan:nth-child(3) .plan__unit--y,
.is-demo .plan--featured {
animation: none;
}
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.pricing {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
width: 100%;
}
.toggle {
display: inline-flex;
gap: 4px;
padding: 4px;
border-radius: 999px;
background: rgba(23, 20, 26, 0.18);
}
.toggle__btn {
border: 0;
border-radius: 999px;
padding: 4px 16px;
font-family: inherit;
font-weight: 600;
font-size: 11px;
line-height: 1.8;
cursor: pointer;
transition: background 300ms ease-out, color 300ms ease-out;
}
.toggle__btn.is-active {
background: #fff7e6;
color: #c93000;
}
.toggle__btn:not(.is-active) {
background: transparent;
color: #fff7e6;
}
.plans {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
width: 100%;
max-width: 420px;
}
.plan {
position: relative;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 12px 8px;
border-radius: 16px;
background: #ffffff;
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
}
.plan--featured {
z-index: 1;
background: #17141a;
transform: scale(1.08);
}
.is-demo .plan--featured {
animation: featured-scale-in 600ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
}
@keyframes featured-scale-in {
0% {
transform: scale(1);
}
100% {
transform: scale(1.08);
}
}
.plan__badge {
margin: 0;
padding: 4px 8px;
border-radius: 4px;
background: #fff7e6;
color: #c93000;
font-weight: 700;
font-size: 9px;
line-height: 1.4;
}
.plan__name {
margin: 0;
color: #17141a;
font-weight: 600;
font-size: 12px;
line-height: 1.4;
}
.plan--featured .plan__name {
color: #ffffff;
}
.plan__price {
position: relative;
margin: 0;
min-height: 20px;
width: 100%;
text-align: center;
}
.plan__amount {
position: absolute;
inset: 0;
color: #17141a;
font-weight: 700;
font-size: 15px;
line-height: 1.4;
white-space: nowrap;
opacity: 0;
transition: opacity 300ms ease-out;
}
.plan--featured .plan__amount {
color: #ffffff;
}
.plan__amount--m {
opacity: 1;
}
.plan__unit-row {
position: relative;
width: 100%;
height: 13px;
text-align: center;
}
.plan__unit {
position: absolute;
inset: 0;
color: rgba(23, 20, 26, 0.72);
font-weight: 400;
font-size: 9px;
line-height: 1.4;
white-space: nowrap;
opacity: 0;
transition: opacity 300ms ease-out;
}
.plan--featured .plan__unit {
color: rgba(255, 255, 255, 0.78);
}
.plan__unit--m {
opacity: 1;
}
.pricing.is-yearly .plan__amount--m,
.pricing.is-yearly .plan__unit--m {
opacity: 0;
}
.pricing.is-yearly .plan__amount--y,
.pricing.is-yearly .plan__unit--y {
opacity: 1;
}
.is-demo .plans > .plan:nth-child(1) .plan__amount--m,
.is-demo .plans > .plan:nth-child(1) .plan__unit--m {
animation: fade-m-1 2s steps(1, end) infinite;
}
.is-demo .plans > .plan:nth-child(1) .plan__amount--y,
.is-demo .plans > .plan:nth-child(1) .plan__unit--y {
animation: fade-y-1 2s steps(1, end) infinite;
}
.is-demo .plans > .plan:nth-child(2) .plan__amount--m,
.is-demo .plans > .plan:nth-child(2) .plan__unit--m {
animation: fade-m-2 2s steps(1, end) infinite;
}
.is-demo .plans > .plan:nth-child(2) .plan__amount--y,
.is-demo .plans > .plan:nth-child(2) .plan__unit--y {
animation: fade-y-2 2s steps(1, end) infinite;
}
.is-demo .plans > .plan:nth-child(3) .plan__amount--m,
.is-demo .plans > .plan:nth-child(3) .plan__unit--m {
animation: fade-m-3 2s steps(1, end) infinite;
}
.is-demo .plans > .plan:nth-child(3) .plan__amount--y,
.is-demo .plans > .plan:nth-child(3) .plan__unit--y {
animation: fade-y-3 2s steps(1, end) infinite;
}
@keyframes fade-m-1 {
0%, 25% {
opacity: 1;
}
30%, 100% {
opacity: 0;
}
}
@keyframes fade-y-1 {
0%, 25% {
opacity: 0;
}
30%, 100% {
opacity: 1;
}
}
@keyframes fade-m-2 {
0%, 45% {
opacity: 1;
}
50%, 100% {
opacity: 0;
}
}
@keyframes fade-y-2 {
0%, 45% {
opacity: 0;
}
50%, 100% {
opacity: 1;
}
}
@keyframes fade-m-3 {
0%, 65% {
opacity: 1;
}
70%, 100% {
opacity: 0;
}
}
@keyframes fade-y-3 {
0%, 65% {
opacity: 0;
}
70%, 100% {
opacity: 1;
}
}
@media (max-width: 340px) {
.plans {
gap: 4px;
max-width: 300px;
}
.plan {
padding: 8px 4px;
}
.plan__amount {
font-size: 12px;
}
}
@media (prefers-reduced-motion: reduce) {
.is-demo .plans > .plan:nth-child(1) .plan__amount--m,
.is-demo .plans > .plan:nth-child(1) .plan__amount--y,
.is-demo .plans > .plan:nth-child(1) .plan__unit--m,
.is-demo .plans > .plan:nth-child(1) .plan__unit--y,
.is-demo .plans > .plan:nth-child(2) .plan__amount--m,
.is-demo .plans > .plan:nth-child(2) .plan__amount--y,
.is-demo .plans > .plan:nth-child(2) .plan__unit--m,
.is-demo .plans > .plan:nth-child(2) .plan__unit--y,
.is-demo .plans > .plan:nth-child(3) .plan__amount--m,
.is-demo .plans > .plan:nth-child(3) .plan__amount--y,
.is-demo .plans > .plan:nth-child(3) .plan__unit--m,
.is-demo .plans > .plan:nth-child(3) .plan__unit--y,
.is-demo .plan--featured {
animation: none;
}
}// Billing cycle 버튼을 누르면 요금제 상태가 연간 또는 Monthly 상태가 되고, 그 상태에 따라 화면에 보이는 금액 값을 다시 계산해 표시한다.
// 계산 겹: Billing cycle 상태 -> 표시 금액 값 재계산. 실사용 클릭 리스너 있음.
(function () {
var root = document.getElementById('pricing');
var mBtn = document.getElementById('btn-monthly');
var yBtn = document.getElementById('btn-yearly');
if (!root || !mBtn || !yBtn) return;
function setYearly(yearly) {
root.classList.remove('is-demo');
root.classList.toggle('is-yearly', yearly);
mBtn.classList.toggle('is-active', !yearly);
yBtn.classList.toggle('is-active', yearly);
mBtn.setAttribute('aria-pressed', String(!yearly));
yBtn.setAttribute('aria-pressed', String(yearly));
}
mBtn.addEventListener('click', function () { setYearly(false); });
yBtn.addEventListener('click', function () { setYearly(true); });
})();import { useEffect, useRef } from "react";
import styles from "./PricingCards.module.scss";
// 결제 주기 버튼을 누르면 요금제 상태가 연간 또는 월간 상태가 되고, 그 상태에 따라 화면에 보이는 금액 값을 다시 계산해 표시한다.
// 계산 겹: 결제 주기 상태 -> 표시 금액 값 재계산. 실사용 클릭 리스너 있음.
const PLANS = [
{ name: "스타터", amountM: "9,000", amountY: "7,200", featured: false },
{ name: "프로", amountM: "19,000", amountY: "15,200", featured: true },
{ name: "비즈니스", amountM: "39,000", amountY: "31,200", featured: false },
];
export default function PricingCards() {
const rootRef = useRef<HTMLDivElement>(null);
const mBtnRef = useRef<HTMLButtonElement>(null);
const yBtnRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
const root = rootRef.current;
const mBtn = mBtnRef.current;
const yBtn = yBtnRef.current;
if (!root || !mBtn || !yBtn) return;
function setYearly(yearly: boolean) {
root!.classList.remove(styles["is-demo"]);
root!.classList.toggle(styles["is-yearly"], yearly);
mBtn!.classList.toggle(styles["is-active"], !yearly);
yBtn!.classList.toggle(styles["is-active"], yearly);
mBtn!.setAttribute("aria-pressed", String(!yearly));
yBtn!.setAttribute("aria-pressed", String(yearly));
}
function onMonthly() { setYearly(false); }
function onYearly() { setYearly(true); }
mBtn.addEventListener("click", onMonthly);
yBtn.addEventListener("click", onYearly);
return () => {
mBtn.removeEventListener("click", onMonthly);
yBtn.removeEventListener("click", onYearly);
};
}, []);
return (
<main className={styles.stage}>
<div ref={rootRef} className={`${styles.pricing} ${styles["is-demo"]}`}>
<div className={styles.toggle} role="group" aria-label="결제 주기">
<button
ref={mBtnRef}
className={`${styles.toggle__btn} ${styles["toggle__btn--m"]} ${styles["is-active"]}`}
type="button"
aria-pressed="true"
>
월간
</button>
<button
ref={yBtnRef}
className={`${styles.toggle__btn} ${styles["toggle__btn--y"]}`}
type="button"
aria-pressed="false"
>
연간 -20%
</button>
</div>
<div className={styles.plans}>
{PLANS.map((plan) => (
<article
key={plan.name}
className={plan.featured ? `${styles.plan} ${styles["plan--featured"]}` : styles.plan}
>
{plan.featured && <p className={styles.plan__badge}>추천</p>}
<p className={styles.plan__name}>{plan.name}</p>
<div className={styles.plan__price}>
<span className={`${styles.plan__amount} ${styles["plan__amount--m"]}`}>{plan.amountM}</span>
<span className={`${styles.plan__amount} ${styles["plan__amount--y"]}`}>{plan.amountY}</span>
</div>
<div className={styles["plan__unit-row"]}>
<span className={`${styles.plan__unit} ${styles["plan__unit--m"]}`}>원/월</span>
<span className={`${styles.plan__unit} ${styles["plan__unit--y"]}`}>원/월(연 결제)</span>
</div>
</article>
))}
</div>
</div>
</main>
);
}03 Pricing cards — a list where only the recommended card gets a larger scale value — Measured area 5.337% · moving frames 9/23Original by GODRICH · SCSS and React ship in the zip · Hero Section Design Examples: 9 Patterns
The post this demo lives in
Hero Section Design Examples: 9 PatternsHero section design examples: One sets a card reveal delay from its order value past a visibility threshold; one resizes See all demos in this post
If you like this effect - similar demos
Landing sections09 Checkout button waits, then shows the resultPricing Table Design: 9 Live Interactions
Landing sections04 Tabbed about-me cardCSS Card Design Examples — 9 Content Card Types
Landing sections04 Profile detail — a card that measures bio height to decide whether read-more even appears9 Team Page Design Patterns (Copy-Paste)