07法人窓口CTA — 入力した利用量からその場で見積もりを求めるセクション
想定人数や利用量を入力するとその値を単価に掛けて月額の見積もりをその場で求めて示し、一定の基準を超えると相談窓口への案内文に変わります。
使いどころ法人向けプラン相談セクション、大量利用の見積もりCTA
aria-valuenowclientWidthrole="slider"

<main class="stage">
<section class="panel">
<p class="hint">使用量スライダーの値から予想料金を再計算し、基準を超えると営業問い合わせに切り替えます。</p>
<div class="track" id="track">
<div class="fill" id="fill"></div>
<div class="knob" id="knob" role="slider" aria-label="月間使用量" aria-valuemin="0" aria-valuemax="1500" aria-valuenow="50"></div>
</div>
<p class="usageLine"><span id="usageNum">50</span><span class="usageUnit"> 件 / 月</span></p>
<p class="estimate" id="estimate">予想料金 23,000ウォン</p>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-enter;
$easing: $ease-spring;
$color: $subject-cream;
@include stage($stage-orange);
.panel {
width: 100%;
max-width: 360px;
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-2;
}
.hint {
margin: 0;
padding: 0 $sp-2;
text-align: center;
font-weight: 400;
font-size: 11px;
line-height: 1.4;
color: rgba($subject-ink, .72);
}
.track {
position: relative;
width: 100%;
height: 6px;
border-radius: $r-pill;
background: rgba($subject-ink, .18);
}
.fill {
position: absolute;
top: 0;
left: 0;
height: 100%;
border-radius: $r-pill;
background: $subject-ink;
width: 0%;
transition: width $duration $easing;
}
.knob {
position: absolute;
top: 50%;
left: 0;
width: 16px;
height: 16px;
margin-top: -8px;
border-radius: $r-pill;
background: $color;
box-shadow: $shadow-press;
transition: left $duration $easing;
}
.usageLine {
margin: 0;
font-weight: 700;
font-size: 16px;
color: $subject-ink;
}
.usageUnit {
font-weight: 400;
font-size: 11px;
color: rgba($subject-ink, .70);
}
.estimate {
margin: 0;
padding: $sp-1 $sp-3;
border-radius: $r-pill;
background: $subject-ink;
color: $subject-cream;
font-weight: 700;
font-size: 12px;
transition: background $duration $easing;
}
.estimate.is-contact {
background: $subject-orange-deep;
}
@media (max-width: 340px) {
.panel { max-width: 280px; gap: $sp-1; }
.hint { font-size: 9px; }
.usageLine { font-size: 13px; }
.estimate { font-size: 11px; }
}
@media (prefers-reduced-motion: reduce) {
.fill,
.knob,
.estimate {
transition: 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);
}
.panel {
width: 100%;
max-width: 360px;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.hint {
margin: 0;
padding: 0 8px;
text-align: center;
font-weight: 400;
font-size: 11px;
line-height: 1.4;
color: rgba(23, 20, 26, 0.72);
}
.track {
position: relative;
width: 100%;
height: 6px;
border-radius: 999px;
background: rgba(23, 20, 26, 0.18);
}
.fill {
position: absolute;
top: 0;
left: 0;
height: 100%;
border-radius: 999px;
background: #17141a;
width: 0%;
transition: width 600ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.knob {
position: absolute;
top: 50%;
left: 0;
width: 16px;
height: 16px;
margin-top: -8px;
border-radius: 999px;
background: #fff7e6;
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
transition: left 600ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.usageLine {
margin: 0;
font-weight: 700;
font-size: 16px;
color: #17141a;
}
.usageUnit {
font-weight: 400;
font-size: 11px;
color: rgba(23, 20, 26, 0.7);
}
.estimate {
margin: 0;
padding: 4px 12px;
border-radius: 999px;
background: #17141a;
color: #fff7e6;
font-weight: 700;
font-size: 12px;
transition: background 600ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.estimate.is-contact {
background: #c93000;
}
@media (max-width: 340px) {
.panel {
max-width: 280px;
gap: 4px;
}
.hint {
font-size: 9px;
}
.usageLine {
font-size: 13px;
}
.estimate {
font-size: 11px;
}
}
@media (prefers-reduced-motion: reduce) {
.fill,
.knob,
.estimate {
transition: none;
}
}// 엔터프라이즈 CTA(07) — 사용량 슬라이더 값(usage)에 건당 단가(RATE)를 곱해 기본요금(BASE)에 더한
// 예상 요금을 매번 새로 구한다. 사용량이 THRESHOLD 를 넘으면 요금 대신 영업 문의 문구로 바꾼다(계산 한 겹).
// 슬라이더 손잡이 위치는 트랙 실제 폭을 재 px 로 계산해 left 에 대입한다(translateX(-50%) 미사용).
(function () {
var track = document.getElementById('track');
var fill = document.getElementById('fill');
var knob = document.getElementById('knob');
var usageNum = document.getElementById('usageNum');
var estimate = document.getElementById('estimate');
if (!track || !fill || !knob || !usageNum || !estimate) return;
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var MAX_USAGE = 1500;
var BASE = 19000;
var RATE = 18;
var THRESHOLD = 1000;
var STEPS = [50, 300, 800, 1200];
function apply(usage) {
var pct = usage / MAX_USAGE;
var trackWidth = track.clientWidth;
var knobWidth = knob.offsetWidth || 16;
var knobLeft = pct * (trackWidth - knobWidth);
knob.style.left = knobLeft + 'px';
fill.style.width = (pct * 100) + '%';
knob.setAttribute('aria-valuenow', String(usage));
usageNum.textContent = String(usage);
if (usage > THRESHOLD) {
estimate.textContent = '営業チームにお問い合わせください';
estimate.classList.add('is-contact');
} else {
var won = BASE + usage * RATE;
estimate.textContent = '予想料金 ' + won.toLocaleString('ko-KR') + 'ウォン';
estimate.classList.remove('is-contact');
}
}
apply(STEPS[0]);
if (!reduce) {
var idx = 0;
setInterval(function () {
idx = (idx + 1) % STEPS.length;
apply(STEPS[idx]);
}, 600);
}
window.addEventListener('resize', function () {
apply(Number(knob.getAttribute('aria-valuenow')) || STEPS[0]);
});
})();// PricingEnterpriseCta — vanilla 판(run/339/demo/07-pricing-enterprise-cta/index.html) 의 React 포트.
// 사용량 슬라이더 값(usage)에 건당 단가(RATE)를 곱해 기본요금(BASE)에 더한 예상 요금을 매번 새로
// 구한다. 사용량이 THRESHOLD 를 넘으면 요금 대신 영업 문의 문구로 바꾼다(계산 한 겹). 슬라이더
// 손잡이 위치는 트랙 실제 폭을 재 px 로 계산해 left 에 대입한다(translateX(-50%) 미사용).
import { useEffect, useRef } from "react";
import styles from "./PricingEnterpriseCta.module.scss";
export type PricingEnterpriseCtaProps = {
duration?: string;
easing?: string;
color?: string;
};
const HINT = "사용량 슬라이더 값으로 예상 요금을 다시 계산하고, 기준을 넘으면 영업 문의로 바꿉니다";
const SLIDER_ARIA = "월간 사용량";
const MAX_USAGE = 1500;
const BASE = 19000;
const RATE = 18;
const THRESHOLD = 1000;
const STEPS = [50, 300, 800, 1200];
export default function PricingEnterpriseCta({
duration = "600ms",
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#fff7e6"
}: PricingEnterpriseCtaProps) {
const trackRef = useRef<HTMLDivElement | null>(null);
const fillRef = useRef<HTMLDivElement | null>(null);
const knobRef = useRef<HTMLDivElement | null>(null);
const usageNumRef = useRef<HTMLSpanElement | null>(null);
const estimateRef = useRef<HTMLParagraphElement | null>(null);
useEffect(() => {
const track = trackRef.current;
const fill = fillRef.current;
const knob = knobRef.current;
const usageNum = usageNumRef.current;
const estimate = estimateRef.current;
if (!track || !fill || !knob || !usageNum || !estimate) return;
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
function apply(usage: number): void {
const pct = usage / MAX_USAGE;
const trackWidth = track!.clientWidth;
const knobWidth = knob!.offsetWidth || 16;
const knobLeft = pct * (trackWidth - knobWidth);
knob!.style.left = knobLeft + "px";
fill!.style.width = pct * 100 + "%";
knob!.setAttribute("aria-valuenow", String(usage));
usageNum!.textContent = String(usage);
if (usage > THRESHOLD) {
estimate!.textContent = "영업팀에 문의해 주세요";
estimate!.classList.add(styles.isContact);
} else {
const won = BASE + usage * RATE;
estimate!.textContent = "예상 요금 " + won.toLocaleString("ko-KR") + "원";
estimate!.classList.remove(styles.isContact);
}
}
apply(STEPS[0]);
let timer = 0;
if (!reduce) {
let idx = 0;
timer = window.setInterval(() => {
idx = (idx + 1) % STEPS.length;
apply(STEPS[idx]);
}, 600);
}
const onResize = (): void => {
apply(Number(knob!.getAttribute("aria-valuenow")) || STEPS[0]);
};
window.addEventListener("resize", onResize);
return () => {
if (timer) window.clearInterval(timer);
window.removeEventListener("resize", onResize);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<main className={styles.stage}>
<section className={styles.panel}>
<p className={styles.hint}>{HINT}</p>
<div className={styles.track} ref={trackRef}>
<div className={styles.fill} ref={fillRef} />
<div
className={styles.knob}
ref={knobRef}
role="slider"
aria-label={SLIDER_ARIA}
aria-valuemin={0}
aria-valuemax={MAX_USAGE}
aria-valuenow={50}
/>
</div>
<p className={styles.usageLine}>
<span ref={usageNumRef}>50</span>
<span className={styles.usageUnit}> 건 / 월</span>
</p>
<p className={styles.estimate} ref={estimateRef}>
예상 요금 23,000원
</p>
</section>
</main>
);
}07 法人窓口CTA — 入力した利用量からその場で見積もりを求めるセクション — 計測 累積面積 2.287% · 動いたフレーム 8/23GODRICH オリジナル · SCSS と React は zip に同梱 · 価格表 デザイン9選 — 年払い切替・割引率まで計算して見せる
このデモが属する記事
価格表 デザイン9選 — 年払い切替・割引率まで計算して見せる価格表 デザインは、料金プランのボタン一つ・数字一つに計算の層を重ねる考え方です。年払い・月払いの切替が変わるたびに各プランの価格を計算し直し、割引バッジは定価と割引価格の差を自分で求め、法人窓口セクションは利用量の値からその場で見積もりを この記事のデモをすべて見る


