08申込CTA
버튼이 자동 재생에서 은은하게 펄스하며 체크 아이콘 그리기를 반복하는 동안, 실제로 이메일을 넣고 제출하면 버튼에 체크 아이콘이 그려지고 그 아래 '등록 완료' 문구가 따로 나타납니다.
使いどころ
stroke-dashoffsetopacitysubmit

<main class="stage">
<section class="rc is-demo">
<span class="rc__orb rc__orb--1" aria-hidden="true"></span>
<span class="rc__orb rc__orb--2" aria-hidden="true"></span>
<h2 class="rc__title">今すぐ登録して<br>早割特典を受け取る</h2>
<p class="rc__sub">登録者には資料集とネットワーキング招待状をお渡しします。</p>
<form class="rc__form" novalidate>
<input class="rc__input" type="email" placeholder="[email protected]" required>
<button class="rc__submit" type="submit">
<span class="rc__submit-label">登録する</span>
<svg class="rc__check" viewBox="0 0 24 24" aria-hidden="true"><polyline points="5 13 10 18 19 7" /></svg>
</button>
</form>
<p class="rc__done" role="status"></p>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-base; // 체크·버튼 전환
$easing: $ease-pop;
$color: $subject-blue; // 버튼·체크 색
@include stage($stage-paper);
.rc {
position: relative;
width: min(360px, 92vw);
text-align: center;
overflow: hidden;
isolation: isolate;
}
.rc__orb {
position: absolute; border-radius: 50%; filter: blur(20px); z-index: -1;
}
.rc__orb--1 { width: 140px; height: 140px; left: -30px; top: -20px; background: radial-gradient(circle, $subject-blue, transparent 70%); opacity: .16; animation: rc-drift 8s ease-in-out infinite; }
.rc__orb--2 { width: 120px; height: 120px; right: -20px; bottom: -30px; background: radial-gradient(circle, $subject-blue, transparent 70%); opacity: .12; animation: rc-drift 7s ease-in-out infinite reverse; }
@keyframes rc-drift {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate($sp-4, -$sp-3); }
}
.rc__title { margin: 0 0 $sp-2; font-weight: 800; font-size: clamp(17px, 5vw, 21px); line-height: 1.3; font-family: inherit; color: $subject-ink; }
.rc__sub { margin: 0 0 $sp-4; font-weight: 500; font-size: clamp(11px, 3vw, 12px); line-height: 1.5; font-family: inherit; color: rgba(23, 20, 26, .6); }
.rc__form { display: flex; gap: $sp-2; justify-content: center; flex-wrap: wrap; }
.rc__input {
flex: 1 1 160px;
max-width: 220px;
padding: $sp-3 $sp-4;
border-radius: $r-control;
border: 1px solid rgba(23, 20, 26, .18);
font-weight: 500; font-size: 12px; line-height: 1; font-family: inherit;
}
.rc__input.is-shake { animation: rc-shake $dur-quick $ease-out; }
@keyframes rc-shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
.rc__submit {
display: inline-flex; align-items: center; gap: $sp-1;
padding: $sp-3 $sp-5;
border-radius: $r-control;
border: 0;
background: $subject-blue-deep;
color: $subject-cream;
font-weight: 700; font-size: 12px; line-height: 1; font-family: inherit;
cursor: pointer;
transition: transform $duration $easing;
}
.rc__check {
width: 14px; height: 14px;
fill: none;
stroke: currentColor;
stroke-width: 2.5;
stroke-linecap: round;
stroke-linejoin: round;
stroke-dasharray: 24;
stroke-dashoffset: 24;
opacity: 0;
transition: stroke-dashoffset $duration $easing, opacity $duration $easing;
}
.rc__form.is-done .rc__submit { transform: scale(1.04); }
.rc__form.is-done .rc__check { stroke-dashoffset: 0; opacity: 1; }
.rc__form.is-done .rc__submit-label::after { content: " 완료"; }
.rc__done { min-height: 14px; margin: $sp-2 0 0; font-weight: 600; font-size: 11px; line-height: 1.4; font-family: inherit; color: $color; }
// ── 데모 루프: 클릭 전까지 버튼이 맴돌고 체크가 그려졌다 지워진다 ──────────────
.rc.is-demo .rc__submit { animation: rc-pulse $dur-loop $easing infinite; }
.rc.is-demo .rc__check { animation: rc-check $dur-loop $easing infinite; }
@keyframes rc-pulse {
0%, 55%, 100% { transform: scale(1); }
70% { transform: scale(1.06); }
}
@keyframes rc-check {
0%, 55% { stroke-dashoffset: 24; opacity: 0; }
72%, 92% { stroke-dashoffset: 0; opacity: 1; }
100% { stroke-dashoffset: 24; opacity: 0; }
}
// ── 반응형: 세로가 짧은 화면에선 여백을 줄인다(BD9) ──────────────
@media (max-width: 340px), (max-height: 220px) {
.rc__title { font-size: 15px; margin-bottom: $sp-1; }
.rc__sub { margin-bottom: $sp-2; font-size: 10px; }
.rc__input { padding: $sp-2 $sp-3; }
.rc__submit { padding: $sp-2 $sp-4; }
.rc__done { min-height: 0; margin-top: $sp-1; }
}
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.rc__orb { animation: none !important; }
.rc__submit, .rc__check, .rc__input { transition: none; animation: none !important; }
.rc.is-demo .rc__submit, .rc.is-demo .rc__check { animation: none !important; }
}@charset "UTF-8";
html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.rc {
position: relative;
width: min(360px, 92vw);
text-align: center;
overflow: hidden;
isolation: isolate;
}
.rc__orb {
position: absolute;
border-radius: 50%;
filter: blur(20px);
z-index: -1;
}
.rc__orb--1 {
width: 140px;
height: 140px;
left: -30px;
top: -20px;
background: radial-gradient(circle, #2f6df6, transparent 70%);
opacity: 0.16;
animation: rc-drift 8s ease-in-out infinite;
}
.rc__orb--2 {
width: 120px;
height: 120px;
right: -20px;
bottom: -30px;
background: radial-gradient(circle, #2f6df6, transparent 70%);
opacity: 0.12;
animation: rc-drift 7s ease-in-out infinite reverse;
}
@keyframes rc-drift {
0%, 100% {
transform: translate(0, 0);
}
50% {
transform: translate(16px, -12px);
}
}
.rc__title {
margin: 0 0 8px;
font-weight: 800;
font-size: clamp(17px, 5vw, 21px);
line-height: 1.3;
font-family: inherit;
color: #17141a;
}
.rc__sub {
margin: 0 0 16px;
font-weight: 500;
font-size: clamp(11px, 3vw, 12px);
line-height: 1.5;
font-family: inherit;
color: rgba(23, 20, 26, 0.6);
}
.rc__form {
display: flex;
gap: 8px;
justify-content: center;
flex-wrap: wrap;
}
.rc__input {
flex: 1 1 160px;
max-width: 220px;
padding: 12px 16px;
border-radius: 12px;
border: 1px solid rgba(23, 20, 26, 0.18);
font-weight: 500;
font-size: 12px;
line-height: 1;
font-family: inherit;
}
.rc__input.is-shake {
animation: rc-shake 220ms ease-out;
}
@keyframes rc-shake {
0%, 100% {
transform: translateX(0);
}
25% {
transform: translateX(-5px);
}
75% {
transform: translateX(5px);
}
}
.rc__submit {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 12px 20px;
border-radius: 12px;
border: 0;
background: #0a4fe8;
color: #fff7e6;
font-weight: 700;
font-size: 12px;
line-height: 1;
font-family: inherit;
cursor: pointer;
transition: transform 300ms cubic-bezier(0.2, 1.4, 0.4, 1);
}
.rc__check {
width: 14px;
height: 14px;
fill: none;
stroke: currentColor;
stroke-width: 2.5;
stroke-linecap: round;
stroke-linejoin: round;
stroke-dasharray: 24;
stroke-dashoffset: 24;
opacity: 0;
transition: stroke-dashoffset 300ms cubic-bezier(0.2, 1.4, 0.4, 1), opacity 300ms cubic-bezier(0.2, 1.4, 0.4, 1);
}
.rc__form.is-done .rc__submit {
transform: scale(1.04);
}
.rc__form.is-done .rc__check {
stroke-dashoffset: 0;
opacity: 1;
}
.rc__form.is-done .rc__submit-label::after {
content: " 완료";
}
.rc__done {
min-height: 14px;
margin: 8px 0 0;
font-weight: 600;
font-size: 11px;
line-height: 1.4;
font-family: inherit;
color: #2f6df6;
}
.rc.is-demo .rc__submit {
animation: rc-pulse 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
.rc.is-demo .rc__check {
animation: rc-check 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
@keyframes rc-pulse {
0%, 55%, 100% {
transform: scale(1);
}
70% {
transform: scale(1.06);
}
}
@keyframes rc-check {
0%, 55% {
stroke-dashoffset: 24;
opacity: 0;
}
72%, 92% {
stroke-dashoffset: 0;
opacity: 1;
}
100% {
stroke-dashoffset: 24;
opacity: 0;
}
}
@media (max-width: 340px), (max-height: 220px) {
.rc__title {
font-size: 15px;
margin-bottom: 4px;
}
.rc__sub {
margin-bottom: 8px;
font-size: 10px;
}
.rc__input {
padding: 8px 12px;
}
.rc__submit {
padding: 8px 16px;
}
.rc__done {
min-height: 0;
margin-top: 4px;
}
}
@media (prefers-reduced-motion: reduce) {
.rc__orb {
animation: none !important;
}
.rc__submit, .rc__check, .rc__input {
transition: none;
animation: none !important;
}
.rc.is-demo .rc__submit, .rc.is-demo .rc__check {
animation: none !important;
}
}// 실제 동작: 이메일 형식을 검사해 성공/오류를 실제로 표시한다.
var form = document.querySelector('.rc__form');
var input = document.querySelector('.rc__input');
var done = document.querySelector('.rc__done');
form.addEventListener('submit', function (e) {
e.preventDefault();
document.querySelector('.rc').classList.remove('is-demo');
var ok = /^\S+@\S+\.\S+$/.test(input.value);
if (ok) {
form.classList.add('is-done');
done.textContent = '登録完了 — 確認メールをお送りしました。';
} else {
input.classList.remove('is-shake');
void input.offsetWidth;
input.classList.add('is-shake');
done.textContent = '正しいメールアドレスを入力してください。';
}
});import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./RegisterCta.module.scss";
export type RegisterCtaProps = { duration?: number; easing?: string; color?: string };
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function RegisterCta({ duration = 300, easing = "cubic-bezier(.2,1.4,.4,1)", color = "#2f6df6" }: RegisterCtaProps) {
const [email, setEmail] = useState("");
const [status, setStatus] = useState<"idle" | "error" | "done">("idle");
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
function onSubmit(e: any) {
e.preventDefault();
const ok = /^\S+@\S+\.\S+$/.test(email);
setStatus(ok ? "done" : "error");
}
return (
<section className={styles.rc} style={vars}>
<h2 className={styles.title}>지금 등록하고<br />얼리버드 혜택 받기</h2>
<p className={styles.sub}>등록자에게 자료집과 네트워킹 초대장을 드립니다.</p>
<form className={styles.form} onSubmit={onSubmit}>
<input
className={status === "error" ? `${styles.input} ${styles.shake}` : styles.input}
type="email"
placeholder="[email protected]"
value={email}
onChange={(e: any) => setEmail(e.target.value)}
/>
<button type="submit" className={styles.submit}>
{status === "done" ? "등록 완료" : "등록하기"}
<svg className={styles.check} viewBox="0 0 24 24" aria-hidden="true" style={{ opacity: status === "done" ? 1 : 0 }}>
<polyline points="5 13 10 18 19 7" />
</svg>
</button>
</form>
<p className={styles.done} role="status">
{status === "done" ? "등록 완료 — 확인 메일을 보냈습니다." : status === "error" ? "올바른 이메일을 입력해 주세요." : ""}
</p>
</section>
);
}08 申込CTA — 計測 累積面積 0.857% · 動いたフレーム 9/23GODRICH オリジナル · SCSS と React は zip に同梱 · 【コピペOK】カンファレンス LP セクション テンプレート9選
このデモが属する記事
【コピペOK】カンファレンス LP セクション テンプレート9選カンファレンス LP セクションテンプレートは、ヒーロー・スケジュール・登壇者紹介のようなイベントランディングページ用ブロックを、LPにそのまま貼れる形でまとめたコード集です。9つのうち1つは、実際の目標時刻まで秒単位で減っていくカウントダ この記事のデモをすべて見る


