09プレスフッター
이메일을 넣고 구독 버튼을 누르면 실제로 형식을 검사해 통과하면 체크가 그려지고, 틀리면 입력칸이 좌우로 짧게 흔들립니다.
使いどころ
submitstroke-dashoffsetclick

<main class="stage">
<footer class="pf">
<div class="pf__top">
<p class="pf__heading">プレスリリースをメールで受け取る</p>
<form class="pf__form" novalidate>
<input class="pf__input" type="email" placeholder="[email protected]" aria-label="メール">
<button class="pf__submit" type="submit">
<span class="pf__submit-label">購読する</span>
<svg class="pf__check" viewBox="0 0 20 20" aria-hidden="true"><polyline points="4,10 8,14 16,5"/></svg>
</button>
</form>
<p class="pf__status" role="status" aria-live="polite"></p>
</div>
<div class="pf__cols">
<div class="pf__col"><p class="pf__col-title">プレス</p><a href="#">プレスリリース</a><a href="#">メディアキット</a></div>
<div class="pf__col"><p class="pf__col-title">会社</p><a href="#">紹介</a><a href="#">採用</a></div>
<div class="pf__col"><p class="pf__col-title">フォロー</p><a href="#">X</a><a href="#">LinkedIn</a></div>
</div>
</footer>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-base; // 체크·버튼 전환
$easing: $ease-pop;
$color: $subject-blue; // 버튼·체크 색
@include stage($stage-ink);
.pf { width: min(440px, 96vw); }
.pf__top { text-align: center; margin-bottom: $sp-4; }
.pf__heading {
margin: 0 0 $sp-2;
font-weight: 700; font-size: clamp(11px, 3.2vw, 12px); line-height: 1.4; font-family: inherit;
color: $subject-cream;
}
.pf__form { display: flex; gap: $sp-2; justify-content: center; flex-wrap: wrap; }
.pf__input {
flex: 1 1 160px;
max-width: 200px;
padding: $sp-2 $sp-3;
border-radius: $r-control;
border: 1px solid rgba(255, 247, 230, .24);
background: rgba(255, 247, 230, .06);
color: $subject-cream;
font-weight: 500; font-size: 11px; line-height: 1; font-family: inherit;
}
.pf__input::placeholder { color: rgba(255, 247, 230, .4); }
.pf__input.is-shake { animation: pf-shake $dur-quick $ease-out; }
@keyframes pf-shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
.pf__submit {
position: relative;
display: inline-flex; align-items: center; gap: $sp-1;
padding: $sp-2 $sp-4;
border-radius: $r-control;
border: 0;
background: $subject-blue-deep;
color: $subject-cream;
font-weight: 700; font-size: 11px; line-height: 1; font-family: inherit;
cursor: pointer;
transition: transform $duration $easing;
animation: pf-glow 2s ease-in-out infinite;
}
@keyframes pf-glow {
0%, 100% { box-shadow: 0 0 0 0 rgba(47, 109, 246, .35); }
50% { box-shadow: 0 0 0 6px rgba(47, 109, 246, 0); }
}
.pf__check {
width: 12px; height: 12px;
fill: none; stroke: currentColor; stroke-width: 2.5;
stroke-linecap: round; stroke-linejoin: round;
stroke-dasharray: 20; stroke-dashoffset: 20;
opacity: 0;
transition: stroke-dashoffset $duration $easing, opacity $duration $easing;
}
.pf__form.is-done .pf__submit { transform: scale(1.04); }
.pf__form.is-done .pf__check { stroke-dashoffset: 0; opacity: 1; }
.pf__form.is-done .pf__submit-label { display: none; }
.pf__status { min-height: 14px; margin: $sp-2 0 0; font-weight: 600; font-size: 10px; line-height: 1.4; font-family: inherit; color: $subject-cream; }
.pf__cols { display: flex; justify-content: center; gap: $sp-6; padding-top: $sp-3; border-top: 1px solid rgba(255, 247, 230, .12); }
.pf__col { display: flex; flex-direction: column; gap: $sp-1; }
.pf__col-title { margin: 0 0 $sp-1; font-weight: 700; font-size: 9px; line-height: 1; font-family: inherit; letter-spacing: .08em; color: rgba($subject-cream, .72); }
.pf__col a { font-weight: 500; font-size: 10px; line-height: 1.6; font-family: inherit; color: rgba(255, 247, 230, .72); text-decoration: none; }
// ── 반응형(BD9) ──────────────────────────────────────────
@media (max-width: 340px), (max-height: 220px) {
.pf__top { margin-bottom: $sp-2; }
.pf__cols { gap: $sp-3; padding-top: $sp-2; }
.pf__col a { font-size: 9px; }
}
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.pf__submit { animation: none !important; }
.pf__input, .pf__check { transition: none; animation: none !important; }
}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);
}
.pf {
width: min(440px, 96vw);
}
.pf__top {
text-align: center;
margin-bottom: 16px;
}
.pf__heading {
margin: 0 0 8px;
font-weight: 700;
font-size: clamp(11px, 3.2vw, 12px);
line-height: 1.4;
font-family: inherit;
color: #fff7e6;
}
.pf__form {
display: flex;
gap: 8px;
justify-content: center;
flex-wrap: wrap;
}
.pf__input {
flex: 1 1 160px;
max-width: 200px;
padding: 8px 12px;
border-radius: 12px;
border: 1px solid rgba(255, 247, 230, 0.24);
background: rgba(255, 247, 230, 0.06);
color: #fff7e6;
font-weight: 500;
font-size: 11px;
line-height: 1;
font-family: inherit;
}
.pf__input::placeholder {
color: rgba(255, 247, 230, 0.4);
}
.pf__input.is-shake {
animation: pf-shake 220ms ease-out;
}
@keyframes pf-shake {
0%, 100% {
transform: translateX(0);
}
25% {
transform: translateX(-5px);
}
75% {
transform: translateX(5px);
}
}
.pf__submit {
position: relative;
display: inline-flex;
align-items: center;
gap: 4px;
padding: 8px 16px;
border-radius: 12px;
border: 0;
background: #0a4fe8;
color: #fff7e6;
font-weight: 700;
font-size: 11px;
line-height: 1;
font-family: inherit;
cursor: pointer;
transition: transform 300ms cubic-bezier(0.2, 1.4, 0.4, 1);
animation: pf-glow 2s ease-in-out infinite;
}
@keyframes pf-glow {
0%, 100% {
box-shadow: 0 0 0 0 rgba(47, 109, 246, 0.35);
}
50% {
box-shadow: 0 0 0 6px rgba(47, 109, 246, 0);
}
}
.pf__check {
width: 12px;
height: 12px;
fill: none;
stroke: currentColor;
stroke-width: 2.5;
stroke-linecap: round;
stroke-linejoin: round;
stroke-dasharray: 20;
stroke-dashoffset: 20;
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);
}
.pf__form.is-done .pf__submit {
transform: scale(1.04);
}
.pf__form.is-done .pf__check {
stroke-dashoffset: 0;
opacity: 1;
}
.pf__form.is-done .pf__submit-label {
display: none;
}
.pf__status {
min-height: 14px;
margin: 8px 0 0;
font-weight: 600;
font-size: 10px;
line-height: 1.4;
font-family: inherit;
color: #fff7e6;
}
.pf__cols {
display: flex;
justify-content: center;
gap: 24px;
padding-top: 12px;
border-top: 1px solid rgba(255, 247, 230, 0.12);
}
.pf__col {
display: flex;
flex-direction: column;
gap: 4px;
}
.pf__col-title {
margin: 0 0 4px;
font-weight: 700;
font-size: 9px;
line-height: 1;
font-family: inherit;
letter-spacing: 0.08em;
color: rgba(255, 247, 230, 0.72);
}
.pf__col a {
font-weight: 500;
font-size: 10px;
line-height: 1.6;
font-family: inherit;
color: rgba(255, 247, 230, 0.72);
text-decoration: none;
}
@media (max-width: 340px), (max-height: 220px) {
.pf__top {
margin-bottom: 8px;
}
.pf__cols {
gap: 12px;
padding-top: 8px;
}
.pf__col a {
font-size: 9px;
}
}
@media (prefers-reduced-motion: reduce) {
.pf__submit {
animation: none !important;
}
.pf__input, .pf__check {
transition: none;
animation: none !important;
}
}// 실제 동작: 제출 시 실제로 メール 형식을 검사한다. 통과하면 체크가 그려지고, 틀리면 입력칸이 흔들린다.
var form = document.querySelector('.pf__form');
var input = document.querySelector('.pf__input');
var status = document.querySelector('.pf__status');
form.addEventListener('submit', function (e) {
e.preventDefault();
var ok = /^\S+@\S+\.\S+$/.test(input.value);
if (ok) {
form.classList.add('is-done');
status.textContent = '購読完了 — 確認メールを送りました。';
} else {
input.classList.remove('is-shake');
void input.offsetWidth;
input.classList.add('is-shake');
status.textContent = '正しいメールアドレスを入力してください。';
}
});import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./PressFooter.module.scss";
export type PressFooterProps = { duration?: number; easing?: string; color?: string };
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function PressFooter({ duration = 300, easing = "cubic-bezier(.2,1.4,.4,1)", color = "#0a4fe8" }: PressFooterProps) {
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 (
<footer className={styles.pf} style={vars}>
<div className={styles.top}>
<p className={styles.heading}>새 보도자료를 메일로 받아보세요</p>
<form className={styles.form} onSubmit={onSubmit}>
<input
className={status === "error" ? `${styles.input} ${styles.shake}` : styles.input}
type="email"
placeholder="[email protected]"
aria-label="이메일"
value={email}
onChange={(e: any) => setEmail(e.target.value)}
/>
<button type="submit" className={styles.submit}>
{status !== "done" && <span className={styles.submitLabel}>구독</span>}
<svg className={styles.check} viewBox="0 0 20 20" aria-hidden="true" style={{ opacity: status === "done" ? 1 : 0 }}>
<polyline points="4,10 8,14 16,5" />
</svg>
</button>
</form>
<p className={styles.status} role="status" aria-live="polite">
{status === "done" ? "구독 완료 — 확인 메일을 보냈습니다." : status === "error" ? "올바른 이메일을 입력해 주세요." : ""}
</p>
</div>
<div className={styles.cols}>
<div className={styles.col}><p className={styles.colTitle}>프레스</p><a href="#">보도자료</a><a href="#">미디어 키트</a></div>
<div className={styles.col}><p className={styles.colTitle}>회사</p><a href="#">소개</a><a href="#">채용</a></div>
<div className={styles.col}><p className={styles.colTitle}>팔로우</p><a href="#">X</a><a href="#">LinkedIn</a></div>
</div>
</footer>
);
}09 プレスフッター — 計測 累積面積 0.578% · 動いたフレーム 15/23GODRICH オリジナル · SCSS と React は zip に同梱 · プレスキット テンプレート付き プレスルームセクション9選
このデモが属する記事
プレスキット テンプレート付き プレスルームセクション9選プレスルーム・メディアページのセクションテンプレートは、プレスリリース一覧・メディアキット・問い合わせ先といった要素をひとまとめにしたプレスキット テンプレート集です。LPにそのまま貼れるヒーローから購読フッターまで、9つのうち1つは実際に この記事のデモをすべて見る


