プレスキット テンプレート付き プレスルームセクション9選
プレスルーム・メディアページのセクションテンプレートは、プレスリリース一覧・メディアキット・問い合わせ先といった要素をひとまとめにしたプレスキット テンプレート集です。LPにそのまま貼れるヒーローから購読フッターまで、9つのうち1つは実際にメールアドレスをクリップボードへコピーするボタンを備えています。
01 press-hero
02 press-release-list
03 press-media-kit
04 press-mentions-bar
05 press-stat-counter
06 press-contact
07 press-founder-quote
08 press-milestone-timeline
09 press-footer
自動再生 · タイルを押すとその項目へ · すべて1つのzip
9つは人気順ではなく、記者・投資家がプレスルームを読み進める順番に並べました。まず第一印象を与え(ヒーロー)、最新情報を確認させ(プレスリリース一覧)、取材に使う資料を渡し(メディアキット)、すでに掲載された実績を見せ(掲載メディア)、規模を数字で裏付け(数字ハイライト)、担当者へすぐ連絡させ(問い合わせ)、ブランドの声を届け(創業者の言葉)、これまでの歩みを示した上で(沿革タイムライン)、次の情報を受け取らせます(フッター)。担当者の顔・メディアロゴの位置は、実際の画像ではなくすべてこのサイト共通の$subject-ink・$subject-blue・$subject-creamトークンだけで描いた図形のプレースホルダーです。ファイルだけ差し替えれば、レイアウトはそのまま残ります。プレスリリース 書き方 テンプレートのような文章の型はここには入っておらず、配っているのは画面のセクションだけです。発表が催しと結び付いているならイベントページのテンプレートが前の席を埋め、報道ではなく成果物を並べる場所はプロジェクト紹介のテンプレートです。
01プレスヒーロー
漂う円と固定の見出し
背景でぼかした円3つがtranslate+scaleでずっと漂う間、図形のワードマーク・見出し・サブ見出し・CTAボタン2つが1画面に収まります。

<main class="stage">
<section class="ph">
<span class="ph__orb ph__orb--1" aria-hidden="true"></span>
<span class="ph__orb ph__orb--2" aria-hidden="true"></span>
<span class="ph__orb ph__orb--3" aria-hidden="true"></span>
<span class="ph__mark" aria-hidden="true"></span>
<p class="ph__eyebrow">PRESS ROOM</p>
<h1 class="ph__title">プレス・パートナー向け<br>最新情報</h1>
<p class="ph__sub">プレスリリース・メディアキット・連絡先をここで確認できます。</p>
<div class="ph__row">
<a class="ph__cta" href="#">リリースを見る</a>
<button class="ph__sub-btn" type="button" aria-pressed="false">
<span class="ph__sub-ic" aria-hidden="true"></span>
リリースを購読
</button>
</div>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-quick; // 구독 버튼 전환
$easing: $ease-pop;
$color: $subject-blue; // CTA·아이콘 강조색
@include stage($stage-ink);
.ph {
position: relative;
box-sizing: border-box; // width 에 패딩을 포함 — 아니면 320px 에서 패딩만큼 넘친다(실측 2026-09-10)
width: min(400px, 92vw);
overflow: hidden;
border-radius: $r-card;
padding: $sp-3 $sp-5 $sp-2;
text-align: center;
isolation: isolate;
}
// ── 배경 원 세 개: 계속 떠다니는 장식(호버 아님, 무조건 재생) ──────────────
.ph__orb {
position: absolute;
border-radius: 50%;
filter: blur(18px);
opacity: .55;
z-index: -1;
}
.ph__orb--1 { width: 170px; height: 170px; left: -45px; top: -35px; background: radial-gradient(circle, $subject-blue, transparent 62%); opacity: 1; filter: blur(3px); animation: ph-float-a 2s ease-in-out infinite; }
.ph__orb--2 { width: 130px; height: 130px; right: -25px; top: 6px; background: radial-gradient(circle, $subject-cream, transparent 62%); opacity: .7; filter: blur(3px); animation: ph-float-b 2s ease-in-out infinite; }
.ph__orb--3 { width: 150px; height: 150px; left: 28%; bottom: -70px; background: radial-gradient(circle, $subject-blue, transparent 62%); opacity: .95; filter: blur(3px); animation: ph-float-a 2s ease-in-out infinite reverse; }
@keyframes ph-float-a {
0%, 100% { transform: translate(0, 0) scale(1); }
50% { transform: translate($sp-10, -$sp-8) scale(1.5); }
}
@keyframes ph-float-b {
0%, 100% { transform: translate(0, 0) scale(1); }
50% { transform: translate(-$sp-8, $sp-8) scale(.6); }
}
// ── 플레이스홀더 워드마크: 실제 로고가 아니라 도형 하나 ──────────────
.ph__mark {
display: block;
width: $sp-5; height: $sp-5;
margin: 0 auto $sp-1;
background: $color;
border-radius: $r-xs;
transform: rotate(45deg);
}
.ph__eyebrow {
margin: 0 0 $sp-1;
font-weight: 700; font-size: 10px; line-height: 1; font-family: inherit;
letter-spacing: .14em;
color: rgba(255, 247, 230, .6);
}
.ph__title {
margin: 0 0 $sp-1;
font-weight: 800; font-size: clamp(16px, 4.6vw, 19px); line-height: 1.14; font-family: inherit;
color: $subject-cream;
}
.ph__sub {
margin: 0 0 $sp-2;
font-weight: 500; font-size: clamp(10px, 2.8vw, 11px); line-height: 1.4; font-family: inherit;
color: rgba(255, 247, 230, .68);
}
.ph__row { display: flex; justify-content: center; align-items: center; gap: $sp-2; flex-wrap: wrap; }
.ph__cta {
padding: $sp-2 $sp-5;
border-radius: $r-control;
background: $subject-blue-deep;
color: $subject-cream;
font-weight: 700; font-size: 12px; line-height: 1.4; font-family: inherit;
text-decoration: none;
}
.ph__sub-btn {
display: inline-flex; align-items: center; gap: $sp-1;
padding: $sp-2 $sp-3;
border-radius: $r-control;
border: 1px solid rgba(255, 247, 230, .3);
background: transparent;
color: rgba(255, 247, 230, .82);
font-weight: 600; font-size: 12px; line-height: 1.4; font-family: inherit;
cursor: pointer;
transition: background $duration $easing, border-color $duration $easing, transform $duration $easing;
}
.ph__sub-ic {
width: $sp-3; height: $sp-3;
border-radius: 0;
background: currentColor;
clip-path: polygon(50% 0%, 63% 38%, 100% 38%, 70% 60%, 82% 100%, 50% 76%, 18% 100%, 30% 60%, 0% 38%, 37% 38%);
opacity: .5;
transition: opacity $duration $easing, transform $duration $easing;
}
.ph__sub-btn[aria-pressed="true"] {
background: rgba(255, 247, 230, .12);
border-color: $subject-cream;
}
.ph__sub-btn[aria-pressed="true"] .ph__sub-ic {
opacity: 1;
color: $color;
transform: scale(1.15);
}
// ── 반응형: 320×200 처럼 세로가 짧은 화면에선 장식을 줄인다(BD9) ──────────────
@media (max-width: 340px), (max-height: 220px) {
.ph { padding: $sp-2 $sp-4; }
.ph__mark { display: none; }
.ph__eyebrow { display: none; }
.ph__title { font-size: 14px; line-height: 1.15; margin-bottom: $sp-1; }
.ph__sub { display: none; }
.ph__row { gap: $sp-1; }
.ph__cta, .ph__sub-btn { padding: 4px $sp-3; font-size: 10px; }
}
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.ph__orb { animation: none !important; }
.ph__sub-btn, .ph__sub-ic { transition: none; }
}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);
}
.ph {
position: relative;
box-sizing: border-box;
width: min(400px, 92vw);
overflow: hidden;
border-radius: 16px;
padding: 12px 20px 8px;
text-align: center;
isolation: isolate;
}
.ph__orb {
position: absolute;
border-radius: 50%;
filter: blur(18px);
opacity: 0.55;
z-index: -1;
}
.ph__orb--1 {
width: 170px;
height: 170px;
left: -45px;
top: -35px;
background: radial-gradient(circle, #2f6df6, transparent 62%);
opacity: 1;
filter: blur(3px);
animation: ph-float-a 2s ease-in-out infinite;
}
.ph__orb--2 {
width: 130px;
height: 130px;
right: -25px;
top: 6px;
background: radial-gradient(circle, #fff7e6, transparent 62%);
opacity: 0.7;
filter: blur(3px);
animation: ph-float-b 2s ease-in-out infinite;
}
.ph__orb--3 {
width: 150px;
height: 150px;
left: 28%;
bottom: -70px;
background: radial-gradient(circle, #2f6df6, transparent 62%);
opacity: 0.95;
filter: blur(3px);
animation: ph-float-a 2s ease-in-out infinite reverse;
}
@keyframes ph-float-a {
0%, 100% {
transform: translate(0, 0) scale(1);
}
50% {
transform: translate(40px, -32px) scale(1.5);
}
}
@keyframes ph-float-b {
0%, 100% {
transform: translate(0, 0) scale(1);
}
50% {
transform: translate(-32px, 32px) scale(0.6);
}
}
.ph__mark {
display: block;
width: 20px;
height: 20px;
margin: 0 auto 4px;
background: #2f6df6;
border-radius: 4px;
transform: rotate(45deg);
}
.ph__eyebrow {
margin: 0 0 4px;
font-weight: 700;
font-size: 10px;
line-height: 1;
font-family: inherit;
letter-spacing: 0.14em;
color: rgba(255, 247, 230, 0.6);
}
.ph__title {
margin: 0 0 4px;
font-weight: 800;
font-size: clamp(16px, 4.6vw, 19px);
line-height: 1.14;
font-family: inherit;
color: #fff7e6;
}
.ph__sub {
margin: 0 0 8px;
font-weight: 500;
font-size: clamp(10px, 2.8vw, 11px);
line-height: 1.4;
font-family: inherit;
color: rgba(255, 247, 230, 0.68);
}
.ph__row {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.ph__cta {
padding: 8px 20px;
border-radius: 12px;
background: #0a4fe8;
color: #fff7e6;
font-weight: 700;
font-size: 12px;
line-height: 1.4;
font-family: inherit;
text-decoration: none;
}
.ph__sub-btn {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 8px 12px;
border-radius: 12px;
border: 1px solid rgba(255, 247, 230, 0.3);
background: transparent;
color: rgba(255, 247, 230, 0.82);
font-weight: 600;
font-size: 12px;
line-height: 1.4;
font-family: inherit;
cursor: pointer;
transition: background 220ms cubic-bezier(0.2, 1.4, 0.4, 1), border-color 220ms cubic-bezier(0.2, 1.4, 0.4, 1), transform 220ms cubic-bezier(0.2, 1.4, 0.4, 1);
}
.ph__sub-ic {
width: 12px;
height: 12px;
border-radius: 0;
background: currentColor;
clip-path: polygon(50% 0%, 63% 38%, 100% 38%, 70% 60%, 82% 100%, 50% 76%, 18% 100%, 30% 60%, 0% 38%, 37% 38%);
opacity: 0.5;
transition: opacity 220ms cubic-bezier(0.2, 1.4, 0.4, 1), transform 220ms cubic-bezier(0.2, 1.4, 0.4, 1);
}
.ph__sub-btn[aria-pressed=true] {
background: rgba(255, 247, 230, 0.12);
border-color: #fff7e6;
}
.ph__sub-btn[aria-pressed=true] .ph__sub-ic {
opacity: 1;
color: #2f6df6;
transform: scale(1.15);
}
@media (max-width: 340px), (max-height: 220px) {
.ph {
padding: 8px 16px;
}
.ph__mark {
display: none;
}
.ph__eyebrow {
display: none;
}
.ph__title {
font-size: 14px;
line-height: 1.15;
margin-bottom: 4px;
}
.ph__sub {
display: none;
}
.ph__row {
gap: 4px;
}
.ph__cta, .ph__sub-btn {
padding: 4px 12px;
font-size: 10px;
}
}
@media (prefers-reduced-motion: reduce) {
.ph__orb {
animation: none !important;
}
.ph__sub-btn, .ph__sub-ic {
transition: none;
}
}// 실제 동작: 클릭마다 購読する 상태가 실제로 바뀐다(아이콘 채움 + aria-pressed).
var btn = document.querySelector('.ph__sub-btn');
btn.addEventListener('click', function () {
var on = btn.getAttribute('aria-pressed') === 'true';
btn.setAttribute('aria-pressed', String(!on));
});import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./PressHero.module.scss";
export type PressHeroProps = {
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function PressHero({ duration = 220, easing = "cubic-bezier(.2,1.4,.4,1)", color = "#2f6df6" }: PressHeroProps) {
const [subscribed, setSubscribed] = useState(false);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.ph} style={vars}>
<span className={`${styles.orb} ${styles.orb1}`} aria-hidden="true" />
<span className={`${styles.orb} ${styles.orb2}`} aria-hidden="true" />
<span className={`${styles.orb} ${styles.orb3}`} aria-hidden="true" />
<span className={styles.mark} aria-hidden="true" />
<p className={styles.eyebrow}>PRESS ROOM</p>
<h1 className={styles.title}>언론과 파트너를 위한<br />최신 소식</h1>
<p className={styles.sub}>보도자료·미디어 키트·연락처를 한 곳에서 확인하세요.</p>
<div className={styles.row}>
<a className={styles.cta} href="#">보도자료 보기</a>
<button
type="button"
className={styles.subBtn}
aria-pressed={subscribed}
onClick={() => setSubscribed((v) => !v)}
>
<span className={styles.subIc} aria-hidden="true" />
보도자료 구독
</button>
</div>
</section>
);
}01 · プレスヒーロー — 計測 累積面積 30.411% · 動いたフレーム 18/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
購読ボタンは実際に切り替わる
右の「プレスリリース購読」ボタンはクリックのたびにaria-pressedが実際に切り替わり、アイコンが塗りつぶされます。
.ph__orb--1 { animation: ph-float-a 2s ease-in-out infinite; }
@keyframes ph-float-a {
0%, 100% { transform: translate(0, 0) scale(1); }
50% { transform: translate($sp-10, -$sp-8) scale(1.5); }
}
.ph__sub-btn[aria-pressed="true"] {
background: rgba(255, 247, 230, .12);
border-color: $subject-cream;
}
.ph__sub-btn[aria-pressed="true"] .ph__sub-ic {
opacity: 1;
color: $color;
transform: scale(1.15);
}
02プレスリリース一覧
プレスリリース テンプレート を分類で絞る
分類チップ(すべて・製品・パートナーシップ・受賞)をクリックすると、その分類ではない行がmax-height+opacity遷移で実際に折りたたまれます。

<main class="stage">
<section class="rl is-demo">
<div class="rl__filters" role="group" aria-label="分類">
<button class="rl__chip" type="button" data-cat="all" aria-pressed="true">すべて</button>
<button class="rl__chip" type="button" data-cat="product" aria-pressed="false">製品</button>
<button class="rl__chip" type="button" data-cat="partner" aria-pressed="false">提携</button>
<button class="rl__chip" type="button" data-cat="award" aria-pressed="false">受賞</button>
</div>
<ul class="rl__list">
<li class="rl__row" data-cat="product"><span class="rl__date">08.20</span><span class="rl__title">新ワークスペース機能を公開</span><span class="rl__tag">製品</span></li>
<li class="rl__row" data-cat="partner"><span class="rl__date">07.02</span><span class="rl__title">B社と販売提携を締結</span><span class="rl__tag">提携</span></li>
<li class="rl__row" data-cat="award"><span class="rl__date">06.15</span><span class="rl__title">今年のスタートアップ賞を受賞</span><span class="rl__tag">受賞</span></li>
<li class="rl__row" data-cat="product"><span class="rl__date">05.30</span><span class="rl__title">バージョン3.0を公開</span><span class="rl__tag">製品</span></li>
</ul>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-base; // 칩·행 전환
$easing: $ease-spring;
$color: $subject-ink; // 강조 악센트 색
@include stage($stage-yellow);
.rl {
box-sizing: border-box;
width: min(400px, 94vw);
background: $subject-cream;
border-radius: $r-card;
padding: $sp-2 $sp-4;
}
.rl__filters { display: flex; gap: $sp-1; flex-wrap: wrap; margin-bottom: $sp-2; }
.rl__chip {
padding: $sp-1 $sp-3;
border-radius: $r-pill;
border: 1px solid rgba(23, 20, 26, .18);
background: transparent;
color: rgba(23, 20, 26, .6);
font-weight: 600; font-size: 10px; line-height: 1.6; font-family: inherit;
cursor: pointer;
transition: background $duration $easing, color $duration $easing, border-color $duration $easing;
}
.rl__chip[aria-pressed="true"] {
background: $color;
border-color: $color;
color: $subject-cream;
}
.rl__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: $sp-1; }
.rl__row {
position: relative;
box-sizing: border-box;
display: flex; align-items: center; gap: $sp-2;
padding: $sp-2 $sp-2 $sp-2 $sp-3;
border-radius: $r-sm;
background: rgba(23, 20, 26, .04);
overflow: hidden;
max-height: 40px;
opacity: 1;
transition: max-height $duration $easing, opacity $duration $easing, padding $duration $easing, margin $duration $easing;
}
.rl__row::before {
content: "";
position: absolute; left: 0; top: 0; bottom: 0;
width: 3px;
background: $subject-blue;
}
.rl__row.is-hidden { max-height: 0; opacity: 0; padding-top: 0; padding-bottom: 0; margin: 0; }
.rl__date { flex: 0 0 auto; font-weight: 700; font-size: 10px; line-height: 1; font-family: inherit; color: rgba($subject-ink, .72); }
.rl__title {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 600; font-size: clamp(11px, 3vw, 12px); line-height: 1.35; font-family: inherit;
color: $subject-ink;
}
.rl__tag {
flex: 0 0 auto;
padding: $sp-1 $sp-2;
border-radius: $r-xs;
background: rgba(47, 109, 246, .12);
color: $subject-blue-deep;
font-weight: 700; font-size: 9px; line-height: 1.6; font-family: inherit;
}
// ── 데모 루프: 행이 순서대로 살짝 밝아졌다 돌아온다(항상 재생, 클릭과 무관) ──────────────
.rl.is-demo .rl__row::before { animation: rl-scan 2s $easing infinite; }
.rl.is-demo .rl__row:nth-child(1)::before { animation-delay: 0ms; }
.rl.is-demo .rl__row:nth-child(2)::before { animation-delay: 160ms; }
.rl.is-demo .rl__row:nth-child(3)::before { animation-delay: 320ms; }
.rl.is-demo .rl__row:nth-child(4)::before { animation-delay: 480ms; }
@keyframes rl-scan {
0%, 80%, 100% { transform: scaleY(1); opacity: 1; }
30% { transform: scaleY(1.6); opacity: .5; }
}
// ── 반응형(BD9) ──────────────────────────────────────────
@media (max-width: 340px), (max-height: 220px) {
.rl { padding: $sp-1 $sp-3; }
.rl__filters { margin-bottom: $sp-1; }
.rl__date { display: none; }
.rl__row { padding: $sp-1 $sp-2 $sp-1 $sp-3; max-height: 26px; }
}
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.rl__row { transition: none; }
.rl.is-demo .rl__row::before { 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: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.rl {
box-sizing: border-box;
width: min(400px, 94vw);
background: #fff7e6;
border-radius: 16px;
padding: 8px 16px;
}
.rl__filters {
display: flex;
gap: 4px;
flex-wrap: wrap;
margin-bottom: 8px;
}
.rl__chip {
padding: 4px 12px;
border-radius: 999px;
border: 1px solid rgba(23, 20, 26, 0.18);
background: transparent;
color: rgba(23, 20, 26, 0.6);
font-weight: 600;
font-size: 10px;
line-height: 1.6;
font-family: inherit;
cursor: pointer;
transition: background 300ms cubic-bezier(0.2, 0.8, 0.2, 1), color 300ms cubic-bezier(0.2, 0.8, 0.2, 1), border-color 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.rl__chip[aria-pressed=true] {
background: #17141a;
border-color: #17141a;
color: #fff7e6;
}
.rl__list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.rl__row {
position: relative;
box-sizing: border-box;
display: flex;
align-items: center;
gap: 8px;
padding: 8px 8px 8px 12px;
border-radius: 8px;
background: rgba(23, 20, 26, 0.04);
overflow: hidden;
max-height: 40px;
opacity: 1;
transition: max-height 300ms cubic-bezier(0.2, 0.8, 0.2, 1), opacity 300ms cubic-bezier(0.2, 0.8, 0.2, 1), padding 300ms cubic-bezier(0.2, 0.8, 0.2, 1), margin 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.rl__row::before {
content: "";
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
background: #2f6df6;
}
.rl__row.is-hidden {
max-height: 0;
opacity: 0;
padding-top: 0;
padding-bottom: 0;
margin: 0;
}
.rl__date {
flex: 0 0 auto;
font-weight: 700;
font-size: 10px;
line-height: 1;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
}
.rl__title {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 600;
font-size: clamp(11px, 3vw, 12px);
line-height: 1.35;
font-family: inherit;
color: #17141a;
}
.rl__tag {
flex: 0 0 auto;
padding: 4px 8px;
border-radius: 4px;
background: rgba(47, 109, 246, 0.12);
color: #0a4fe8;
font-weight: 700;
font-size: 9px;
line-height: 1.6;
font-family: inherit;
}
.rl.is-demo .rl__row::before {
animation: rl-scan 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.rl.is-demo .rl__row:nth-child(1)::before {
animation-delay: 0ms;
}
.rl.is-demo .rl__row:nth-child(2)::before {
animation-delay: 160ms;
}
.rl.is-demo .rl__row:nth-child(3)::before {
animation-delay: 320ms;
}
.rl.is-demo .rl__row:nth-child(4)::before {
animation-delay: 480ms;
}
@keyframes rl-scan {
0%, 80%, 100% {
transform: scaleY(1);
opacity: 1;
}
30% {
transform: scaleY(1.6);
opacity: 0.5;
}
}
@media (max-width: 340px), (max-height: 220px) {
.rl {
padding: 4px 12px;
}
.rl__filters {
margin-bottom: 4px;
}
.rl__date {
display: none;
}
.rl__row {
padding: 4px 8px 4px 12px;
max-height: 26px;
}
}
@media (prefers-reduced-motion: reduce) {
.rl__row {
transition: none;
}
.rl.is-demo .rl__row::before {
animation: none !important;
}
}// 실제 동작: 分類 칩을 클릭하면 그 분류가 아닌 행이 실제로 접히고 칩 상태가 배타적으로 바뀐다.
var root = document.querySelector('.rl');
var chips = root.querySelectorAll('.rl__chip');
var rows = root.querySelectorAll('.rl__row');
chips.forEach(function (chip) {
chip.addEventListener('click', function () {
root.classList.remove('is-demo');
chips.forEach(function (c) { c.setAttribute('aria-pressed', 'false'); });
chip.setAttribute('aria-pressed', 'true');
var cat = chip.getAttribute('data-cat');
rows.forEach(function (row) {
var show = cat === 'all' || row.getAttribute('data-cat') === cat;
row.classList.toggle('is-hidden', !show);
});
});
});import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./PressReleaseList.module.scss";
export type PressReleaseListProps = {
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
type Cat = "all" | "product" | "partner" | "award";
const CHIPS: { cat: Cat; label: string }[] = [
{ cat: "all", label: "전체" },
{ cat: "product", label: "제품" },
{ cat: "partner", label: "파트너십" },
{ cat: "award", label: "수상" },
];
const RELEASES: { date: string; title: string; cat: Exclude<Cat, "all">; tag: string }[] = [
{ date: "08.20", title: "새 워크스페이스 기능 출시", cat: "product", tag: "제품" },
{ date: "07.02", title: "B사와 유통 파트너십 체결", cat: "partner", tag: "파트너십" },
{ date: "06.15", title: "올해의 스타트업상 수상", cat: "award", tag: "수상" },
{ date: "05.30", title: "3.0 버전 업데이트 공개", cat: "product", tag: "제품" },
];
export default function PressReleaseList({ duration = 300, easing = "cubic-bezier(.2,.8,.2,1)", color = "#17141a" }: PressReleaseListProps) {
const [active, setActive] = useState<Cat>("all");
const [interacted, setInteracted] = useState(false);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={interacted ? styles.rl : `${styles.rl} ${styles.demo}`} style={vars}>
<div className={styles.filters} role="group" aria-label="분류">
{CHIPS.map((c) => (
<button
key={c.cat}
type="button"
className={styles.chip}
aria-pressed={active === c.cat}
onClick={() => { setInteracted(true); setActive(c.cat); }}
>
{c.label}
</button>
))}
</div>
<ul className={styles.list}>
{RELEASES.map((r) => {
const show = active === "all" || active === r.cat;
return (
<li key={r.title} className={show ? styles.row : `${styles.row} ${styles.hidden}`}>
<span className={styles.date}>{r.date}</span>
<span className={styles.title}>{r.title}</span>
<span className={styles.tag}>{r.tag}</span>
</li>
);
})}
</ul>
</section>
);
}02 · プレスリリース一覧 — 計測 累積面積 0.271% · 動いたフレーム 16/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
アクセントバーのスキャン
各行左側のアクセントバーは常に順番に明るくなるスキャン動作を繰り返します(装飾)。
var root = document.querySelector('.rl');
var chips = root.querySelectorAll('.rl__chip');
var rows = root.querySelectorAll('.rl__row');
chips.forEach(function (chip) {
chip.addEventListener('click', function () {
root.classList.remove('is-demo');
chips.forEach(function (c) { c.setAttribute('aria-pressed', 'false'); });
chip.setAttribute('aria-pressed', 'true');
var cat = chip.getAttribute('data-cat');
rows.forEach(function (row) {
var show = cat === 'all' || row.getAttribute('data-cat') === cat;
row.classList.toggle('is-hidden', !show);
});
});
});
03メディアキット
選んだファイルだけ有効化
チェックボックス3つ(ロゴパック・ブランドガイド・製品スクリーンショット)を選ぶと「選択したファイルを受け取る」ボタンが実際に有効化され(disabled解除)、ラベルに個数が付きます。

<main class="stage">
<section class="mk">
<h2 class="mk__title">メディアキット</h2>
<ul class="mk__list">
<li class="mk__row">
<label class="mk__label">
<input class="mk__check" type="checkbox" value="logo">
<span class="mk__name">ロゴパック(SVG・PNG)</span>
</label>
<span class="mk__size">1.2MB</span>
<svg class="mk__done-ic" viewBox="0 0 20 20" aria-hidden="true"><polyline points="4,10 8,14 16,5"/></svg>
</li>
<li class="mk__row">
<label class="mk__label">
<input class="mk__check" type="checkbox" value="guide">
<span class="mk__name">ブランドガイド(PDF)</span>
</label>
<span class="mk__size">3.4MB</span>
<svg class="mk__done-ic" viewBox="0 0 20 20" aria-hidden="true"><polyline points="4,10 8,14 16,5"/></svg>
</li>
<li class="mk__row">
<label class="mk__label">
<input class="mk__check" type="checkbox" value="shots">
<span class="mk__name">製品スクリーンショット(ZIP)</span>
</label>
<span class="mk__size">4.8MB</span>
<svg class="mk__done-ic" viewBox="0 0 20 20" aria-hidden="true"><polyline points="4,10 8,14 16,5"/></svg>
</li>
</ul>
<button class="mk__btn" type="button" disabled>
<span class="mk__btn-ic" aria-hidden="true"></span>
選択したファイルを取得
</button>
<p class="mk__status" role="status" aria-live="polite"></p>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-base; // 체크·버튼 전환
$easing: $ease-pop;
$color: $subject-blue; // 체크·강조색
@include stage($stage-orange);
.mk {
position: relative;
box-sizing: border-box;
overflow: hidden;
width: min(360px, 94vw);
background: $subject-cream;
border-radius: $r-card;
padding: $sp-3;
}
.mk::before {
content: "";
position: absolute; left: 0; right: 0; top: 0;
height: 3px;
background: $color;
transform-origin: center;
animation: mk-scan 2s $easing infinite;
}
@keyframes mk-scan {
0%, 70%, 100% { transform: scaleX(.25); opacity: .5; }
30% { transform: scaleX(1); opacity: 1; }
}
.mk__title { margin: 0 0 $sp-3; font-weight: 800; font-size: clamp(14px, 4vw, 16px); line-height: 1.2; font-family: inherit; color: $subject-ink; }
.mk__list { list-style: none; margin: 0 0 $sp-3; padding: 0; display: flex; flex-direction: column; gap: $sp-1; }
.mk__row {
display: flex; align-items: center; gap: $sp-2;
padding: $sp-1 $sp-2;
border-radius: $r-sm;
background: rgba(23, 20, 26, .05);
}
.mk__label { display: flex; align-items: center; gap: $sp-2; flex: 1 1 auto; min-width: 0; cursor: pointer; }
.mk__check { width: 16px; height: 16px; accent-color: $color; margin: 0; flex: 0 0 auto; }
.mk__name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 600; font-size: clamp(10px, 3vw, 11px); line-height: 1.3; font-family: inherit;
color: $subject-ink;
}
.mk__size { flex: 0 0 auto; font-weight: 600; font-size: 9px; line-height: 1; font-family: inherit; color: rgba($subject-ink, .72); }
.mk__done-ic {
width: 14px; height: 14px; flex: 0 0 auto;
fill: none;
stroke: $color;
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;
}
.mk__row.is-done .mk__done-ic { stroke-dashoffset: 0; opacity: 1; }
.mk__row.is-done .mk__size { display: none; }
.mk__btn {
display: flex; align-items: center; justify-content: center;
width: 100%;
padding: $sp-2 $sp-4;
border-radius: $r-control;
border: 0;
background: $subject-ink;
color: $subject-cream;
font-weight: 700; font-size: 12px; line-height: 1.4; font-family: inherit;
cursor: pointer;
transition: opacity $duration $easing;
}
.mk__btn:disabled { opacity: .4; cursor: not-allowed; }
.mk__btn-ic {
width: 0; height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 6px solid currentColor;
margin-right: $sp-1;
animation: mk-bounce 2s ease-in-out infinite;
}
@keyframes mk-bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(3px); }
}
.mk__status { min-height: 14px; margin: $sp-2 0 0; font-weight: 600; font-size: 11px; line-height: 1.4; font-family: inherit; color: $color; text-align: center; }
// ── 반응형(BD9) ──────────────────────────────────────────
@media (max-width: 340px), (max-height: 220px) {
.mk { padding: $sp-2; }
.mk__title { display: none; }
.mk__list { margin-bottom: $sp-1; }
.mk__size { display: none; }
.mk__row { padding: $sp-1 $sp-2; }
.mk__btn { padding: $sp-1 $sp-3; }
.mk__status { margin-top: $sp-1; min-height: 0; }
}
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.mk__btn-ic { animation: none !important; }
.mk::before { animation: none !important; }
.mk__done-ic, .mk__btn { 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);
}
.mk {
position: relative;
box-sizing: border-box;
overflow: hidden;
width: min(360px, 94vw);
background: #fff7e6;
border-radius: 16px;
padding: 12px;
}
.mk::before {
content: "";
position: absolute;
left: 0;
right: 0;
top: 0;
height: 3px;
background: #2f6df6;
transform-origin: center;
animation: mk-scan 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
@keyframes mk-scan {
0%, 70%, 100% {
transform: scaleX(0.25);
opacity: 0.5;
}
30% {
transform: scaleX(1);
opacity: 1;
}
}
.mk__title {
margin: 0 0 12px;
font-weight: 800;
font-size: clamp(14px, 4vw, 16px);
line-height: 1.2;
font-family: inherit;
color: #17141a;
}
.mk__list {
list-style: none;
margin: 0 0 12px;
padding: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.mk__row {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 8px;
border-radius: 8px;
background: rgba(23, 20, 26, 0.05);
}
.mk__label {
display: flex;
align-items: center;
gap: 8px;
flex: 1 1 auto;
min-width: 0;
cursor: pointer;
}
.mk__check {
width: 16px;
height: 16px;
accent-color: #2f6df6;
margin: 0;
flex: 0 0 auto;
}
.mk__name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 600;
font-size: clamp(10px, 3vw, 11px);
line-height: 1.3;
font-family: inherit;
color: #17141a;
}
.mk__size {
flex: 0 0 auto;
font-weight: 600;
font-size: 9px;
line-height: 1;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
}
.mk__done-ic {
width: 14px;
height: 14px;
flex: 0 0 auto;
fill: none;
stroke: #2f6df6;
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);
}
.mk__row.is-done .mk__done-ic {
stroke-dashoffset: 0;
opacity: 1;
}
.mk__row.is-done .mk__size {
display: none;
}
.mk__btn {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
padding: 8px 16px;
border-radius: 12px;
border: 0;
background: #17141a;
color: #fff7e6;
font-weight: 700;
font-size: 12px;
line-height: 1.4;
font-family: inherit;
cursor: pointer;
transition: opacity 300ms cubic-bezier(0.2, 1.4, 0.4, 1);
}
.mk__btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.mk__btn-ic {
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 6px solid currentColor;
margin-right: 4px;
animation: mk-bounce 2s ease-in-out infinite;
}
@keyframes mk-bounce {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(3px);
}
}
.mk__status {
min-height: 14px;
margin: 8px 0 0;
font-weight: 600;
font-size: 11px;
line-height: 1.4;
font-family: inherit;
color: #2f6df6;
text-align: center;
}
@media (max-width: 340px), (max-height: 220px) {
.mk {
padding: 8px;
}
.mk__title {
display: none;
}
.mk__list {
margin-bottom: 4px;
}
.mk__size {
display: none;
}
.mk__row {
padding: 4px 8px;
}
.mk__btn {
padding: 4px 12px;
}
.mk__status {
margin-top: 4px;
min-height: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.mk__btn-ic {
animation: none !important;
}
.mk::before {
animation: none !important;
}
.mk__done-ic, .mk__btn {
transition: none;
}
}// 실제 동작: 체크한 ファイル 수만큼 버튼 라벨이 바뀌고, 1개 이상이어야 버튼이 눌린다.
var root = document.querySelector('.mk');
var checks = root.querySelectorAll('.mk__check');
var btn = root.querySelector('.mk__btn');
var status = root.querySelector('.mk__status');
function updateLabel() {
var checkedFiles = Array.prototype.filter.call(checks, function (c) { return c.checked; });
btn.disabled = checkedFiles.length === 0;
btn.lastChild.textContent = checkedFiles.length ? ' 選択したファイルを取得 (' + checkedFiles.length + ')' : ' 選択したファイルを取得';
}
checks.forEach(function (c) { c.addEventListener('change', updateLabel); });
btn.addEventListener('click', function () {
var checkedFiles = Array.prototype.filter.call(checks, function (c) { return c.checked; });
checkedFiles.forEach(function (c) { c.closest('.mk__row').classList.add('is-done'); });
status.textContent = 'ファイル ' + checkedFiles.length + '件を取得しました。';
});import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./PressMediaKit.module.scss";
export type PressMediaKitProps = {
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const FILES = [
{ id: "logo", name: "로고 팩 (SVG · PNG)", size: "1.2MB" },
{ id: "guide", name: "브랜드 가이드 (PDF)", size: "3.4MB" },
{ id: "shots", name: "제품 스크린샷 (ZIP)", size: "4.8MB" },
];
export default function PressMediaKit({ duration = 300, easing = "cubic-bezier(.2,1.4,.4,1)", color = "#2f6df6" }: PressMediaKitProps) {
const [checked, setChecked] = useState<Record<string, boolean>>({});
const [done, setDone] = useState<Record<string, boolean>>({});
const [status, setStatus] = useState("");
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
const count = Object.values(checked).filter(Boolean).length;
function toggle(id: string, e: any) {
setChecked((c) => ({ ...c, [id]: e.target.checked }));
}
function download() {
const next: Record<string, boolean> = {};
FILES.forEach((f) => { if (checked[f.id]) next[f.id] = true; });
setDone((d) => ({ ...d, ...next }));
setStatus(`파일 ${count}개를 받았습니다.`);
}
return (
<section className={styles.mk} style={vars}>
<h2 className={styles.title}>미디어 키트</h2>
<ul className={styles.list}>
{FILES.map((f) => (
<li key={f.id} className={done[f.id] ? `${styles.row} ${styles.done}` : styles.row}>
<label className={styles.label}>
<input className={styles.check} type="checkbox" checked={!!checked[f.id]} onChange={(e: any) => toggle(f.id, e)} />
<span className={styles.name}>{f.name}</span>
</label>
{!done[f.id] && <span className={styles.size}>{f.size}</span>}
<svg className={styles.doneIc} viewBox="0 0 20 20" aria-hidden="true"><polyline points="4,10 8,14 16,5" /></svg>
</li>
))}
</ul>
<button type="button" className={styles.btn} disabled={count === 0} onClick={download}>
<span className={styles.btnIc} aria-hidden="true" />
{count ? ` 선택한 파일 받기 (${count})` : " 선택한 파일 받기"}
</button>
<p className={styles.status} role="status" aria-live="polite">{status}</p>
</section>
);
}03 · メディアキット — 計測 累積面積 0.756% · 動いたフレーム 22/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
クリックで描かれるチェック
クリックすると選んだ行にSVGチェックがstroke-dashoffsetで描かれます。
function updateLabel() {
var checkedFiles = Array.prototype.filter.call(checks, function (c) { return c.checked; });
btn.disabled = checkedFiles.length === 0;
btn.lastChild.textContent = checkedFiles.length ? ' 選択したファイルを取得 (' + checkedFiles.length + ')' : ' 選択したファイルを取得';
}
checks.forEach(function (c) { c.addEventListener('change', updateLabel); });
btn.addEventListener('click', function () {
var checkedFiles = Array.prototype.filter.call(checks, function (c) { return c.checked; });
checkedFiles.forEach(function (c) { c.closest('.mk__row').classList.add('is-done'); });
status.textContent = 'ファイル ' + checkedFiles.length + '件を取得しました。';
});
04掲載メディア
途切れなく流れるトラック
図形で描いたメディアマーク8個(実際のロゴではありません)をつないだトラックがtranslateX(-50%)で途切れなく流れます。

<main class="stage">
<section class="mb" aria-label="メディア掲載">
<p class="mb__label">AS SEEN IN</p>
<div class="mb__viewport">
<div class="mb__track">
<span class="mb__logo mb__logo--1" aria-hidden="true"></span>
<span class="mb__logo mb__logo--2" aria-hidden="true"></span>
<span class="mb__logo mb__logo--3" aria-hidden="true"></span>
<span class="mb__logo mb__logo--4" aria-hidden="true"></span>
<span class="mb__logo mb__logo--1" aria-hidden="true"></span>
<span class="mb__logo mb__logo--2" aria-hidden="true"></span>
<span class="mb__logo mb__logo--3" aria-hidden="true"></span>
<span class="mb__logo mb__logo--4" aria-hidden="true"></span>
</div>
</div>
<button class="mb__pause" type="button" aria-pressed="false">流れを止める</button>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: 14s; // 트랙 한 바퀴 시간 (근접값 아님 — 마퀴 전용 값)
$easing: linear;
$color: $subject-ink; // 로고 마크 색
@include stage($stage-paper);
.mb { width: min(440px, 96vw); text-align: center; }
.mb__label {
margin: 0 0 $sp-3;
font-weight: 700; font-size: 10px; line-height: 1; font-family: inherit;
letter-spacing: .16em;
color: rgba($subject-ink, .72);
}
.mb__viewport {
overflow: hidden;
-webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}
// 트랙: gap 을 쓰지 않는다(BD15) — 항목마다 margin-right 로 간격을 준다
.mb__track {
display: flex;
width: max-content;
animation: mb-scroll $duration $easing infinite;
}
.mb.is-paused .mb__track,
.mb__viewport:hover .mb__track { animation-play-state: paused; }
@keyframes mb-scroll {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
.mb__logo {
flex: 0 0 auto;
width: 64px; height: 32px;
margin-right: $sp-6;
border-radius: $r-sm;
background: rgba(23, 20, 26, .06);
display: flex; align-items: center; justify-content: center;
gap: $sp-1;
}
.mb__logo::before {
content: "";
width: 12px; height: 12px;
background: $color;
}
.mb__logo::after {
content: "";
width: 20px; height: 6px;
border-radius: $r-xs;
background: rgba(23, 20, 26, .28);
}
.mb__logo--1::before { border-radius: 50%; }
.mb__logo--2::before { border-radius: $r-xs; transform: rotate(45deg); }
.mb__logo--3::before { clip-path: polygon(50% 0%, 0% 100%, 100% 100%); border-radius: 0; }
.mb__logo--4::before { border-radius: $r-pill; width: 16px; }
.mb__pause {
margin-top: $sp-3;
padding: $sp-1 $sp-3;
border-radius: $r-pill;
border: 1px solid rgba(23, 20, 26, .18);
background: none;
color: rgba(23, 20, 26, .6);
font-weight: 600; font-size: 10px; line-height: 1.6; font-family: inherit;
cursor: pointer;
}
.mb__pause[aria-pressed="true"] { background: rgba(23, 20, 26, .08); color: $subject-ink; }
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.mb__track { animation: none !important; transform: translateX(0); }
}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);
}
.mb {
width: min(440px, 96vw);
text-align: center;
}
.mb__label {
margin: 0 0 12px;
font-weight: 700;
font-size: 10px;
line-height: 1;
font-family: inherit;
letter-spacing: 0.16em;
color: rgba(23, 20, 26, 0.72);
}
.mb__viewport {
overflow: hidden;
-webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}
.mb__track {
display: flex;
width: max-content;
animation: mb-scroll 14s linear infinite;
}
.mb.is-paused .mb__track,
.mb__viewport:hover .mb__track {
animation-play-state: paused;
}
@keyframes mb-scroll {
from {
transform: translateX(0);
}
to {
transform: translateX(-50%);
}
}
.mb__logo {
flex: 0 0 auto;
width: 64px;
height: 32px;
margin-right: 24px;
border-radius: 8px;
background: rgba(23, 20, 26, 0.06);
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
.mb__logo::before {
content: "";
width: 12px;
height: 12px;
background: #17141a;
}
.mb__logo::after {
content: "";
width: 20px;
height: 6px;
border-radius: 4px;
background: rgba(23, 20, 26, 0.28);
}
.mb__logo--1::before {
border-radius: 50%;
}
.mb__logo--2::before {
border-radius: 4px;
transform: rotate(45deg);
}
.mb__logo--3::before {
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
border-radius: 0;
}
.mb__logo--4::before {
border-radius: 999px;
width: 16px;
}
.mb__pause {
margin-top: 12px;
padding: 4px 12px;
border-radius: 999px;
border: 1px solid rgba(23, 20, 26, 0.18);
background: none;
color: rgba(23, 20, 26, 0.6);
font-weight: 600;
font-size: 10px;
line-height: 1.6;
font-family: inherit;
cursor: pointer;
}
.mb__pause[aria-pressed=true] {
background: rgba(23, 20, 26, 0.08);
color: #17141a;
}
@media (prefers-reduced-motion: reduce) {
.mb__track {
animation: none !important;
transform: translateX(0);
}
}// 실제 동작: 버튼을 누르거나 마우스를 올리면 흐름이 실제로 멈춘다(터치 기기는 버튼으로).
var root = document.querySelector('.mb');
var btn = document.querySelector('.mb__pause');
btn.addEventListener('click', function () {
var on = btn.getAttribute('aria-pressed') === 'true';
btn.setAttribute('aria-pressed', String(!on));
root.classList.toggle('is-paused', !on);
});import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./PressMentionsBar.module.scss";
const SHAPES = ["circle", "diamond", "triangle", "pill"] as const;
export type PressMentionsBarProps = { duration?: number; easing?: string; color?: string };
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function PressMentionsBar({ duration = 14000, easing = "linear", color = "#17141a" }: PressMentionsBarProps) {
const [paused, setPaused] = useState(false);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
const logos = [...SHAPES, ...SHAPES];
return (
<section className={styles.mb} style={vars} aria-label="언론 보도">
<p className={styles.label}>AS SEEN IN</p>
<div className={styles.viewport}>
<div className={paused ? `${styles.track} ${styles.paused}` : styles.track}>
{logos.map((shape, i) => (
<span key={i} className={`${styles.logo} ${styles[shape]}`} aria-hidden="true" />
))}
</div>
</div>
<button
type="button"
className={styles.pause}
aria-pressed={paused}
onClick={() => setPaused((v) => !v)}
>
흐름 멈추기
</button>
</section>
);
}04 · 掲載メディア — 計測 累積面積 8.057% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
その場で止める
カーソルを乗せるか隣のボタンを押すと、その場で流れが止まります。
// トラック自体には gap を使わない — 項目ごとに margin-right で間隔を取る
.mb__track { display: flex; width: max-content; animation: mb-scroll 14s linear infinite; }
.mb__logo { flex: 0 0 auto; margin-right: $sp-6; }
@keyframes mb-scroll {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
05数字ハイライト
0から目標値まで上がる
カード3枚の大きな数字が、ページを開いた瞬間にrequestAnimationFrameで0から実際の目標値までeaseOut曲線で上がって止まります(1.2秒)。

<main class="stage">
<section class="sc">
<div class="sc__card">
<p class="sc__num" data-target="128">0</p>
<p class="sc__label">メディア掲載数</p>
</div>
<div class="sc__card">
<p class="sc__num" data-target="42">0</p>
<p class="sc__label">提携メディア数</p>
</div>
<div class="sc__card">
<p class="sc__num" data-target="3200">0</p>
<p class="sc__label">累計ダウンロード数</p>
</div>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-enter; // 카드 악센트 펄스
$easing: $ease-spring;
$color: $subject-blue; // 숫자·악센트 색
@include stage($stage-ink);
.sc {
display: flex;
gap: $sp-3;
width: min(420px, 94vw);
}
.sc__card {
position: relative;
flex: 0 0 auto;
box-sizing: border-box;
width: calc((100% - #{$sp-3} * 2) / 3);
overflow: hidden;
border-radius: $r-card;
padding: $sp-4 $sp-2 $sp-3;
background: rgba(255, 247, 230, .06);
text-align: center;
}
.sc__card::before {
content: "";
position: absolute; left: 0; right: 0; top: 0;
height: 3px;
background: $color;
transform-origin: center;
animation: sc-pulse 2s $easing infinite;
}
.sc__card:nth-child(1)::before { animation-delay: 0ms; }
.sc__card:nth-child(2)::before { animation-delay: 220ms; }
.sc__card:nth-child(3)::before { animation-delay: 440ms; }
@keyframes sc-pulse {
0%, 70%, 100% { transform: scaleX(.3); opacity: .5; }
30% { transform: scaleX(1); opacity: 1; }
}
.sc__num {
margin: 0 0 $sp-1;
font-weight: 800; font-size: clamp(20px, 7vw, 28px); line-height: 1; font-family: inherit;
color: $subject-cream;
font-variant-numeric: tabular-nums;
}
.sc__label {
margin: 0;
font-weight: 600; font-size: clamp(9px, 2.6vw, 10px); line-height: 1.3; font-family: inherit;
color: rgba(255, 247, 230, .58);
}
// ── 반응형(BD9) ──────────────────────────────────────────
@media (max-width: 340px), (max-height: 220px) {
.sc { gap: $sp-2; }
.sc__card { padding: $sp-2 $sp-1; }
.sc__num { font-size: 16px; }
.sc__label { font-size: 8px; }
}
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.sc__card::before { animation: none !important; transform: scaleX(1); opacity: 1; }
}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);
}
.sc {
display: flex;
gap: 12px;
width: min(420px, 94vw);
}
.sc__card {
position: relative;
flex: 0 0 auto;
box-sizing: border-box;
width: calc((100% - 12px * 2) / 3);
overflow: hidden;
border-radius: 16px;
padding: 16px 8px 12px;
background: rgba(255, 247, 230, 0.06);
text-align: center;
}
.sc__card::before {
content: "";
position: absolute;
left: 0;
right: 0;
top: 0;
height: 3px;
background: #2f6df6;
transform-origin: center;
animation: sc-pulse 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.sc__card:nth-child(1)::before {
animation-delay: 0ms;
}
.sc__card:nth-child(2)::before {
animation-delay: 220ms;
}
.sc__card:nth-child(3)::before {
animation-delay: 440ms;
}
@keyframes sc-pulse {
0%, 70%, 100% {
transform: scaleX(0.3);
opacity: 0.5;
}
30% {
transform: scaleX(1);
opacity: 1;
}
}
.sc__num {
margin: 0 0 4px;
font-weight: 800;
font-size: clamp(20px, 7vw, 28px);
line-height: 1;
font-family: inherit;
color: #fff7e6;
font-variant-numeric: tabular-nums;
}
.sc__label {
margin: 0;
font-weight: 600;
font-size: clamp(9px, 2.6vw, 10px);
line-height: 1.3;
font-family: inherit;
color: rgba(255, 247, 230, 0.58);
}
@media (max-width: 340px), (max-height: 220px) {
.sc {
gap: 8px;
}
.sc__card {
padding: 8px 4px;
}
.sc__num {
font-size: 16px;
}
.sc__label {
font-size: 8px;
}
}
@media (prefers-reduced-motion: reduce) {
.sc__card::before {
animation: none !important;
transform: scaleX(1);
opacity: 1;
}
}// 실제 동작: 페이지가 열리면 0에서 실제 목표값까지 숫자가 실시간으로 올라간다.
var nums = document.querySelectorAll('.sc__num');
var countStart = performance.now();
var countDuration = 1200;
function easeOut(t) { return 1 - Math.pow(1 - t, 3); }
function tick(now) {
var t = Math.min(1, (now - countStart) / countDuration);
var e = easeOut(t);
nums.forEach(function (el) {
var target = Number(el.getAttribute('data-target'));
el.textContent = Math.round(target * e).toLocaleString('ko-KR');
});
if (t < 1) requestAnimationFrame(tick);
}
requestAnimationFrame(tick);import { useEffect, useState } from "react";
import type { CSSProperties } from "react";
import styles from "./PressStatCounter.module.scss";
export type PressStatCounterProps = { duration?: number; easing?: string; color?: string };
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const STATS = [
{ target: 128, label: "언론 보도 건수" },
{ target: 42, label: "협력 매체 수" },
{ target: 3200, label: "누적 다운로드" },
];
export default function PressStatCounter({ duration = 600, easing = "cubic-bezier(.2,.8,.2,1)", color = "#2f6df6" }: PressStatCounterProps) {
const [values, setValues] = useState<number[]>(STATS.map(() => 0));
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
useEffect(() => {
const start = performance.now();
const length = 1200;
let frame: number;
function easeOut(t: number) { return 1 - Math.pow(1 - t, 3); }
function tick(now: number) {
const t = Math.min(1, (now - start) / length);
const e = easeOut(t);
setValues(STATS.map((s) => Math.round(s.target * e)));
if (t < 1) frame = requestAnimationFrame(tick);
}
frame = requestAnimationFrame(tick);
return () => cancelAnimationFrame(frame);
}, []);
return (
<section className={styles.sc} style={vars}>
{STATS.map((s, i) => (
<div key={s.label} className={styles.card}>
<p className={styles.num}>{values[i].toLocaleString("ko-KR")}</p>
<p className={styles.label}>{s.label}</p>
</div>
))}
</section>
);
}05 · 数字ハイライト — 計測 累積面積 2.675% · 動いたフレーム 21/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
カード上のアクセントバー
カード上のアクセントバーは常に順番にスキャン動作を繰り返します。
var countStart = performance.now();
var countDuration = 1200;
function easeOut(t) { return 1 - Math.pow(1 - t, 3); }
function tick(now) {
var t = Math.min(1, (now - countStart) / countDuration);
var e = easeOut(t);
nums.forEach(function (el) {
var target = Number(el.getAttribute('data-target'));
el.textContent = Math.round(target * e).toLocaleString('ko-KR');
});
if (t < 1) requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
06メディア問い合わせ
実際にクリップボードへ
担当者名・肩書きの下のメールボタンを押すと、実際にnavigator.clipboard.writeText()が実行されます。

<main class="stage">
<section class="mc is-demo">
<span class="mc__orb mc__orb--1" aria-hidden="true"></span>
<span class="mc__orb mc__orb--2" aria-hidden="true"></span>
<span class="mc__avatar" aria-hidden="true"></span>
<p class="mc__name">キム・プレス</p>
<p class="mc__role">PR・メディア担当</p>
<button class="mc__copy" type="button" data-email="[email protected]">
<span class="mc__copy-label">[email protected]</span>
<svg class="mc__copy-ic" viewBox="0 0 20 20" aria-hidden="true"><polyline points="4,10 8,14 16,5"/></svg>
</button>
<p class="mc__hint" role="status" aria-live="polite"></p>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-base; // 복사 버튼 전환
$easing: $ease-pop;
$color: $subject-blue; // 버튼·체크 색
@include stage($stage-yellow);
.mc {
position: relative;
box-sizing: border-box; // width 에 패딩을 포함 — 아니면 320px 에서 패딩만큼 넘친다(실측 2026-09-10)
width: min(352px, 92vw); // 320 + 좌우패딩(sp-4×2=32) — border-box 전환 전 데스크톱 박스 크기를 그대로 유지(92vw 쪽은 그대로라 모바일엔 영향 없음)
text-align: center;
overflow: hidden;
isolation: isolate;
border-radius: $r-card;
padding: $sp-5 $sp-4 $sp-4;
}
.mc__orb { position: absolute; border-radius: 50%; filter: blur(16px); z-index: -1; }
.mc__orb--1 { width: 150px; height: 150px; left: -40px; top: -30px; background: radial-gradient(circle, $subject-blue, transparent 68%); opacity: .5; animation: mc-drift 8s ease-in-out infinite; }
.mc__orb--2 { width: 130px; height: 130px; right: -30px; bottom: -40px; background: radial-gradient(circle, $subject-ink, transparent 68%); opacity: .32; animation: mc-drift 7s ease-in-out infinite reverse; }
@keyframes mc-drift {
0%, 100% { transform: translate(0, 0) scale(1); }
50% { transform: translate($sp-6, -$sp-5) scale(1.25); }
}
.mc__avatar {
display: block;
width: 48px; height: 48px;
margin: 0 auto $sp-2;
border-radius: 50%;
background: $subject-ink;
}
.mc__name { margin: 0 0 $sp-1; font-weight: 800; font-size: clamp(14px, 4vw, 16px); line-height: 1.2; font-family: inherit; color: $subject-ink; }
.mc__role { margin: 0 0 $sp-4; font-weight: 500; font-size: clamp(10px, 2.8vw, 11px); line-height: 1.4; font-family: inherit; color: rgba($subject-ink, .72); }
.mc__copy {
display: inline-flex; align-items: center; gap: $sp-1;
padding: $sp-2 $sp-4;
border-radius: $r-control;
border: 1px solid rgba(23, 20, 26, .16);
background: none;
color: $subject-ink;
font-weight: 700; font-size: 11px; line-height: 1; font-family: inherit;
cursor: pointer;
max-width: 100%;
transition: border-color $duration $easing, background $duration $easing;
}
.mc__copy-ic {
width: 12px; height: 12px; flex: 0 0 auto;
fill: none;
stroke: $color;
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;
}
.mc__copy.is-copied { border-color: $color; background: rgba(47, 109, 246, .08); }
.mc__copy.is-copied .mc__copy-ic { stroke-dashoffset: 0; opacity: 1; }
.mc__hint { min-height: 14px; margin: $sp-2 0 0; font-weight: 600; font-size: 10px; line-height: 1.4; font-family: inherit; color: $color; }
// ── 데모 루프: 복사 버튼을 클릭하기 전까지 체크가 옅게 나타났다 사라진다(항상 재생) ──────
.mc.is-demo .mc__copy-ic { animation: mc-hint 2s $easing infinite; }
@keyframes mc-hint {
0%, 60% { stroke-dashoffset: 20; opacity: 0; }
78%, 90% { stroke-dashoffset: 0; opacity: .5; }
100% { stroke-dashoffset: 20; opacity: 0; }
}
// ── 반응형(BD9) ──────────────────────────────────────────
@media (max-width: 340px), (max-height: 220px) {
.mc { padding: $sp-3 $sp-3; }
.mc__avatar { width: 36px; height: 36px; margin-bottom: $sp-1; }
.mc__role { margin-bottom: $sp-2; }
.mc__copy { padding: $sp-1 $sp-3; font-size: 10px; }
}
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.mc__orb { animation: none !important; }
.mc__copy, .mc__copy-ic { transition: none; }
.mc.is-demo .mc__copy-ic { 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: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.mc {
position: relative;
box-sizing: border-box;
width: min(352px, 92vw);
text-align: center;
overflow: hidden;
isolation: isolate;
border-radius: 16px;
padding: 20px 16px 16px;
}
.mc__orb {
position: absolute;
border-radius: 50%;
filter: blur(16px);
z-index: -1;
}
.mc__orb--1 {
width: 150px;
height: 150px;
left: -40px;
top: -30px;
background: radial-gradient(circle, #2f6df6, transparent 68%);
opacity: 0.5;
animation: mc-drift 8s ease-in-out infinite;
}
.mc__orb--2 {
width: 130px;
height: 130px;
right: -30px;
bottom: -40px;
background: radial-gradient(circle, #17141a, transparent 68%);
opacity: 0.32;
animation: mc-drift 7s ease-in-out infinite reverse;
}
@keyframes mc-drift {
0%, 100% {
transform: translate(0, 0) scale(1);
}
50% {
transform: translate(24px, -20px) scale(1.25);
}
}
.mc__avatar {
display: block;
width: 48px;
height: 48px;
margin: 0 auto 8px;
border-radius: 50%;
background: #17141a;
}
.mc__name {
margin: 0 0 4px;
font-weight: 800;
font-size: clamp(14px, 4vw, 16px);
line-height: 1.2;
font-family: inherit;
color: #17141a;
}
.mc__role {
margin: 0 0 16px;
font-weight: 500;
font-size: clamp(10px, 2.8vw, 11px);
line-height: 1.4;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
}
.mc__copy {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 8px 16px;
border-radius: 12px;
border: 1px solid rgba(23, 20, 26, 0.16);
background: none;
color: #17141a;
font-weight: 700;
font-size: 11px;
line-height: 1;
font-family: inherit;
cursor: pointer;
max-width: 100%;
transition: border-color 300ms cubic-bezier(0.2, 1.4, 0.4, 1), background 300ms cubic-bezier(0.2, 1.4, 0.4, 1);
}
.mc__copy-ic {
width: 12px;
height: 12px;
flex: 0 0 auto;
fill: none;
stroke: #2f6df6;
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);
}
.mc__copy.is-copied {
border-color: #2f6df6;
background: rgba(47, 109, 246, 0.08);
}
.mc__copy.is-copied .mc__copy-ic {
stroke-dashoffset: 0;
opacity: 1;
}
.mc__hint {
min-height: 14px;
margin: 8px 0 0;
font-weight: 600;
font-size: 10px;
line-height: 1.4;
font-family: inherit;
color: #2f6df6;
}
.mc.is-demo .mc__copy-ic {
animation: mc-hint 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
@keyframes mc-hint {
0%, 60% {
stroke-dashoffset: 20;
opacity: 0;
}
78%, 90% {
stroke-dashoffset: 0;
opacity: 0.5;
}
100% {
stroke-dashoffset: 20;
opacity: 0;
}
}
@media (max-width: 340px), (max-height: 220px) {
.mc {
padding: 12px 12px;
}
.mc__avatar {
width: 36px;
height: 36px;
margin-bottom: 4px;
}
.mc__role {
margin-bottom: 8px;
}
.mc__copy {
padding: 4px 12px;
font-size: 10px;
}
}
@media (prefers-reduced-motion: reduce) {
.mc__orb {
animation: none !important;
}
.mc__copy, .mc__copy-ic {
transition: none;
}
.mc.is-demo .mc__copy-ic {
animation: none !important;
}
}// 실제 동작: 버튼을 누르면 이메일이 실제로 클립보드에 복사되고, 잠깐 체크 아이콘으로 바뀐 뒤 되돌아간다.
var root = document.querySelector('.mc');
var btn = document.querySelector('.mc__copy');
var hint = document.querySelector('.mc__hint');
var revertTimer = null;
btn.addEventListener('click', function () {
root.classList.remove('is-demo');
var email = btn.getAttribute('data-email');
var showCopied = function () {
btn.classList.add('is-copied');
hint.textContent = 'メールアドレスをコピーしました。';
clearTimeout(revertTimer);
revertTimer = setTimeout(function () { btn.classList.remove('is-copied'); }, 1600);
};
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(email).then(showCopied, showCopied);
} else {
var t = document.createElement('textarea');
t.value = email; document.body.appendChild(t); t.select();
try { document.execCommand('copy'); } catch (e) {}
document.body.removeChild(t);
showCopied();
}
});import { useRef, useState } from "react";
import type { CSSProperties } from "react";
import styles from "./PressContact.module.scss";
export type PressContactProps = { duration?: number; easing?: string; color?: string };
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const EMAIL = "[email protected]";
export default function PressContact({ duration = 300, easing = "cubic-bezier(.2,1.4,.4,1)", color = "#2f6df6" }: PressContactProps) {
const [copied, setCopied] = useState(false);
const [hint, setHint] = useState("");
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
async function onCopy() {
try {
await navigator.clipboard.writeText(EMAIL);
} catch {
// 클립보드 권한이 없어도 상태는 그대로 갱신한다(폴백은 vanilla 판 참고)
}
setCopied(true);
setHint("이메일 주소를 복사했습니다.");
if (timer.current) clearTimeout(timer.current);
timer.current = setTimeout(() => setCopied(false), 1600);
}
return (
<section className={styles.mc} style={vars}>
<span className={`${styles.orb} ${styles.orb1}`} aria-hidden="true" />
<span className={`${styles.orb} ${styles.orb2}`} aria-hidden="true" />
<span className={styles.avatar} aria-hidden="true" />
<p className={styles.name}>김프레스</p>
<p className={styles.role}>PR · 미디어 담당</p>
<button type="button" className={copied ? `${styles.copy} ${styles.copied}` : styles.copy} onClick={onCopy}>
<span className={styles.copyLabel}>{EMAIL}</span>
<svg className={styles.copyIc} viewBox="0 0 20 20" aria-hidden="true"><polyline points="4,10 8,14 16,5" /></svg>
</button>
<p className={styles.hint} role="status" aria-live="polite">{hint}</p>
</section>
);
}06 · メディア問い合わせ — 計測 累積面積 3.774% · 動いたフレーム 6/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
1.6秒後に元へ戻る
ボタンにはSVGチェックがstroke-dashoffsetで描かれてから、1.6秒後に元のラベルへ戻ります。
btn.addEventListener('click', function () {
root.classList.remove('is-demo');
var email = btn.getAttribute('data-email');
var showCopied = function () {
btn.classList.add('is-copied');
hint.textContent = 'メールアドレスをコピーしました。';
clearTimeout(revertTimer);
revertTimer = setTimeout(function () { btn.classList.remove('is-copied'); }, 1600);
};
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(email).then(showCopied, showCopied);
}
});
07創業者の言葉
ドット2個で切り替わる
創業者の引用文2つがドット2個で切り替わります。

<main class="stage">
<section class="fq is-demo">
<span class="fq__mark" aria-hidden="true">"</span>
<div class="fq__stage">
<figure class="fq__slide is-active" data-i="0">
<blockquote class="fq__quote">「小さなチームでも大手メディアと同じ速さで情報を届けられると信じています」</blockquote>
<figcaption class="fq__who"><span class="fq__name">イ・ドヒョン</span> ・共同創業者 兼 CEO</figcaption>
</figure>
<figure class="fq__slide" data-i="1">
<blockquote class="fq__quote">「プレスルームは宣伝ではなく、信頼を確かめる場所です」</blockquote>
<figcaption class="fq__who"><span class="fq__name">パク・ソヨン</span> ・共同創業者 兼 CTO</figcaption>
</figure>
</div>
<div class="fq__dots" role="group" aria-label="引用を選択">
<button class="fq__dot is-active" type="button" data-i="0" aria-selected="true" aria-label="最初の引用"></button>
<button class="fq__dot" type="button" data-i="1" aria-selected="false" aria-label="二番目の引用"></button>
</div>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-enter; // 슬라이드 크로스페이드
$easing: $ease-spring;
$color: $subject-ink; // 인용부호·강조색
@include stage($stage-orange);
.fq {
position: relative;
box-sizing: border-box; // width 에 패딩을 포함 — 아니면 320px 에서 패딩만큼 넘친다(실측 2026-09-10)
width: min(400px, 94vw);
background: $subject-cream;
border-radius: $r-card;
padding: $sp-6 $sp-4 $sp-4;
text-align: center;
overflow: hidden;
}
.fq__mark {
position: absolute;
top: $sp-1; left: 50%;
transform: translateX(-50%);
font: 800 56px/1 Georgia, serif;
color: rgba(47, 109, 246, .32);
animation: fq-wobble 2s $easing infinite;
}
@keyframes fq-wobble {
0%, 100% { transform: translateX(-50%) rotate(0deg) scale(1); }
50% { transform: translateX(-50%) rotate(-10deg) scale(1.35); }
}
.fq__stage { position: relative; min-height: 96px; }
.fq__slide {
position: absolute; inset: 0;
margin: 0;
opacity: 0;
transition: opacity $duration $easing;
pointer-events: none;
}
.fq__slide.is-active { opacity: 1; pointer-events: auto; }
.fq__quote {
margin: 0 0 $sp-2;
font-weight: 700; font-size: clamp(12px, 3.6vw, 14px); line-height: 1.5; font-family: inherit;
color: $subject-ink;
}
.fq__who { font-weight: 600; font-size: 11px; line-height: 1.4; font-family: inherit; color: rgba($subject-ink, .72); }
.fq__name { color: $color; }
.fq__dots { display: flex; justify-content: center; gap: $sp-2; margin-top: $sp-3; }
.fq__dot {
width: 8px; height: 8px;
border-radius: 50%;
border: 0;
padding: 0;
background: rgba(23, 20, 26, .2);
cursor: pointer;
transition: background $duration $easing, transform $duration $easing;
}
.fq__dot.is-active { background: $color; transform: scale(1.3); }
// ── 반응형(BD9) ──────────────────────────────────────────
@media (max-width: 340px), (max-height: 220px) {
.fq { padding: $sp-4 $sp-3 $sp-3; }
.fq__mark { font-size: 28px; }
.fq__stage { min-height: 70px; }
.fq__quote { font-size: 11px; }
}
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.fq__mark { animation: none !important; }
.fq__slide, .fq__dot { 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);
}
.fq {
position: relative;
box-sizing: border-box;
width: min(400px, 94vw);
background: #fff7e6;
border-radius: 16px;
padding: 24px 16px 16px;
text-align: center;
overflow: hidden;
}
.fq__mark {
position: absolute;
top: 4px;
left: 50%;
transform: translateX(-50%);
font: 800 56px/1 Georgia, serif;
color: rgba(47, 109, 246, 0.32);
animation: fq-wobble 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes fq-wobble {
0%, 100% {
transform: translateX(-50%) rotate(0deg) scale(1);
}
50% {
transform: translateX(-50%) rotate(-10deg) scale(1.35);
}
}
.fq__stage {
position: relative;
min-height: 96px;
}
.fq__slide {
position: absolute;
inset: 0;
margin: 0;
opacity: 0;
transition: opacity 600ms cubic-bezier(0.2, 0.8, 0.2, 1);
pointer-events: none;
}
.fq__slide.is-active {
opacity: 1;
pointer-events: auto;
}
.fq__quote {
margin: 0 0 8px;
font-weight: 700;
font-size: clamp(12px, 3.6vw, 14px);
line-height: 1.5;
font-family: inherit;
color: #17141a;
}
.fq__who {
font-weight: 600;
font-size: 11px;
line-height: 1.4;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
}
.fq__name {
color: #17141a;
}
.fq__dots {
display: flex;
justify-content: center;
gap: 8px;
margin-top: 12px;
}
.fq__dot {
width: 8px;
height: 8px;
border-radius: 50%;
border: 0;
padding: 0;
background: rgba(23, 20, 26, 0.2);
cursor: pointer;
transition: background 600ms cubic-bezier(0.2, 0.8, 0.2, 1), transform 600ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.fq__dot.is-active {
background: #17141a;
transform: scale(1.3);
}
@media (max-width: 340px), (max-height: 220px) {
.fq {
padding: 16px 12px 12px;
}
.fq__mark {
font-size: 28px;
}
.fq__stage {
min-height: 70px;
}
.fq__quote {
font-size: 11px;
}
}
@media (prefers-reduced-motion: reduce) {
.fq__mark {
animation: none !important;
}
.fq__slide, .fq__dot {
transition: none;
}
}// 실제 동작: 점을 클릭하면 그 인용문으로 실제로 크로스페이드 전환된다.
var root = document.querySelector('.fq');
var dots = root.querySelectorAll('.fq__dot');
var slides = root.querySelectorAll('.fq__slide');
dots.forEach(function (dot) {
dot.addEventListener('click', function () {
root.classList.remove('is-demo');
var i = dot.getAttribute('data-i');
dots.forEach(function (d) { d.classList.remove('is-active'); d.setAttribute('aria-selected', 'false'); });
slides.forEach(function (s) { s.classList.remove('is-active'); });
dot.classList.add('is-active'); dot.setAttribute('aria-selected', 'true');
root.querySelector('.fq__slide[data-i="' + i + '"]').classList.add('is-active');
});
});import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./PressFounderQuote.module.scss";
export type PressFounderQuoteProps = { duration?: number; easing?: string; color?: string };
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const QUOTES = [
{ quote: "\"작은 팀도 대형 언론과 같은 속도로 소식을 전할 수 있어야 한다고 믿습니다.\"", name: "이도현", role: "공동창업자 & CEO" },
{ quote: "\"프레스룸은 홍보가 아니라 신뢰를 확인시켜주는 자리입니다.\"", name: "박서연", role: "공동창업자 & CTO" },
];
export default function PressFounderQuote({ duration = 600, easing = "cubic-bezier(.2,.8,.2,1)", color = "#17141a" }: PressFounderQuoteProps) {
const [active, setActive] = useState(0);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.fq} style={vars}>
<span className={styles.mark} aria-hidden="true">"</span>
<div className={styles.stage}>
{QUOTES.map((q, i) => (
<figure key={q.name} className={i === active ? `${styles.slide} ${styles.active}` : styles.slide}>
<blockquote className={styles.quote}>{q.quote}</blockquote>
<figcaption className={styles.who}><span className={styles.name}>{q.name}</span> · {q.role}</figcaption>
</figure>
))}
</div>
<div className={styles.dots} role="group" aria-label="인용문 선택">
{QUOTES.map((q, i) => (
<button
key={q.name}
type="button"
className={i === active ? `${styles.dot} ${styles.active}` : styles.dot}
aria-selected={i === active}
aria-label={`${i + 1}번째 인용`}
onClick={() => setActive(i)}
/>
))}
</div>
</section>
);
}07 · 創業者の言葉 — 計測 累積面積 0.366% · 動いたフレーム 17/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
引用符の装飾が揺れる
ドットをクリックするとその引用文へopacityで実際にクロスフェード転換し、大きな引用符の装飾がrotate+scaleで常に揺れます。
.fq__mark {
position: absolute;
top: $sp-1; left: 50%;
transform: translateX(-50%);
font: 800 56px/1 Georgia, serif;
color: rgba(47, 109, 246, .32);
animation: fq-wobble 2s $easing infinite;
}
@keyframes fq-wobble {
0%, 100% { transform: translateX(-50%) rotate(0deg) scale(1); }
50% { transform: translateX(-50%) rotate(-10deg) scale(1.35); }
}
.fq__dot.is-active { background: $color; transform: scale(1.3); }
08沿革タイムライン
クリックした地点まで埋まる
横線上の沿革4地点(創立・シリーズA・海外進出・100万ユーザー)をクリックすると、その地点まで進捗バーがscaleXで実際に埋まります。

<main class="stage">
<section class="mt is-demo" data-active="0">
<p class="mt__desc" role="status">2019年、3人が小さなオフィスで最初の製品をリリースしました。</p>
<div class="mt__track">
<span class="mt__fill" aria-hidden="true"></span>
<ol class="mt__list">
<li><button class="mt__stop" type="button" data-i="0" aria-current="step">
<span class="mt__dot" aria-hidden="true"></span><span class="mt__year">2019</span><span class="mt__label">創業</span>
</button></li>
<li><button class="mt__stop" type="button" data-i="1">
<span class="mt__dot" aria-hidden="true"></span><span class="mt__year">2021</span><span class="mt__label">シリーズA</span>
</button></li>
<li><button class="mt__stop" type="button" data-i="2">
<span class="mt__dot" aria-hidden="true"></span><span class="mt__year">2023</span><span class="mt__label">海外展開</span>
</button></li>
<li><button class="mt__stop" type="button" data-i="3">
<span class="mt__dot" aria-hidden="true"></span><span class="mt__year">2026</span><span class="mt__label">利用者100万人</span>
</button></li>
</ol>
</div>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-base; // 진행바·설명 전환
$easing: $ease-spring;
$color: $subject-blue; // 진행바·현재 지점 색
@include stage($stage-paper);
.mt { width: min(420px, 95vw); }
.mt__desc {
min-height: 32px;
margin: 0 0 $sp-3;
font-weight: 600; font-size: clamp(11px, 3.2vw, 12px); line-height: 1.5; font-family: inherit;
color: $subject-ink;
}
.mt__track { position: relative; }
.mt__track::before {
content: "";
position: absolute; left: 0; right: 0; top: 4px;
height: 3px;
border-radius: $r-xs;
background: rgba(23, 20, 26, .12);
}
.mt__fill {
position: absolute; left: 0; right: 0; top: 4px;
height: 3px;
border-radius: $r-xs;
background: $color;
transform: scaleX(.25);
transform-origin: left; // 규칙8: 진행바 left 채움
transition: transform $duration $easing;
}
.mt__list { position: relative; list-style: none; margin: 0; padding: $sp-3 0 0; display: flex; justify-content: space-between; }
.mt__stop {
display: flex; flex-direction: column; align-items: center; gap: $sp-1;
border: 0; padding: 0; background: none; cursor: pointer;
}
.mt__dot {
width: 10px; height: 10px;
border-radius: 50%;
background: rgba(23, 20, 26, .3);
transition: background $duration $easing, transform $duration $easing, box-shadow $duration $easing;
}
.mt__stop[aria-current="step"] .mt__dot {
background: $color;
transform: scale(1.3);
box-shadow: 0 0 0 4px rgba(47, 109, 246, .18);
}
.mt__year { font-weight: 800; font-size: 10px; line-height: 1; font-family: inherit; color: $subject-ink; }
.mt__label { font-weight: 600; font-size: 9px; line-height: 1.3; font-family: inherit; color: rgba($subject-ink, .72); }
// ── 데모 루프: 현재 지점 점이 계속 펄스한다(항상 재생) ──────────────
.mt.is-demo .mt__stop[aria-current="step"] .mt__dot { animation: mt-pulse 2s $easing infinite; }
@keyframes mt-pulse {
0%, 100% { box-shadow: 0 0 0 4px rgba(47, 109, 246, .18); }
50% { box-shadow: 0 0 0 8px rgba(47, 109, 246, .08); }
}
// ── 반응형(BD9) ──────────────────────────────────────────
@media (max-width: 340px), (max-height: 220px) {
.mt__desc { min-height: 0; margin-bottom: $sp-2; font-size: 10px; }
.mt__label { display: none; }
}
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.mt__fill, .mt__dot { transition: none; }
.mt.is-demo .mt__stop[aria-current="step"] .mt__dot { 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: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.mt {
width: min(420px, 95vw);
}
.mt__desc {
min-height: 32px;
margin: 0 0 12px;
font-weight: 600;
font-size: clamp(11px, 3.2vw, 12px);
line-height: 1.5;
font-family: inherit;
color: #17141a;
}
.mt__track {
position: relative;
}
.mt__track::before {
content: "";
position: absolute;
left: 0;
right: 0;
top: 4px;
height: 3px;
border-radius: 4px;
background: rgba(23, 20, 26, 0.12);
}
.mt__fill {
position: absolute;
left: 0;
right: 0;
top: 4px;
height: 3px;
border-radius: 4px;
background: #2f6df6;
transform: scaleX(0.25);
transform-origin: left;
transition: transform 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.mt__list {
position: relative;
list-style: none;
margin: 0;
padding: 12px 0 0;
display: flex;
justify-content: space-between;
}
.mt__stop {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
border: 0;
padding: 0;
background: none;
cursor: pointer;
}
.mt__dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: rgba(23, 20, 26, 0.3);
transition: background 300ms cubic-bezier(0.2, 0.8, 0.2, 1), transform 300ms cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.mt__stop[aria-current=step] .mt__dot {
background: #2f6df6;
transform: scale(1.3);
box-shadow: 0 0 0 4px rgba(47, 109, 246, 0.18);
}
.mt__year {
font-weight: 800;
font-size: 10px;
line-height: 1;
font-family: inherit;
color: #17141a;
}
.mt__label {
font-weight: 600;
font-size: 9px;
line-height: 1.3;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
}
.mt.is-demo .mt__stop[aria-current=step] .mt__dot {
animation: mt-pulse 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes mt-pulse {
0%, 100% {
box-shadow: 0 0 0 4px rgba(47, 109, 246, 0.18);
}
50% {
box-shadow: 0 0 0 8px rgba(47, 109, 246, 0.08);
}
}
@media (max-width: 340px), (max-height: 220px) {
.mt__desc {
min-height: 0;
margin-bottom: 8px;
font-size: 10px;
}
.mt__label {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.mt__fill, .mt__dot {
transition: none;
}
.mt.is-demo .mt__stop[aria-current=step] .mt__dot {
animation: none !important;
}
}// 실제 동작: 연혁을 클릭하면 그 지점까지 진행바가 scaleX 로 실제로 채워지고 설명 문단이 바뀐다.
var milestoneNotes = [
'2019年、3人が小さなオフィスで最初の製品をリリースしました。',
'2021年、シリーズAの資金調達を受け、チームを10倍に増やしました。',
'2023年、日本・米国に拠点を開き、海外展開を始めました。',
'2026年、累計登録者が100万人を超えました。'
];
var root = document.querySelector('.mt');
var stops = root.querySelectorAll('.mt__stop');
var desc = root.querySelector('.mt__desc');
stops.forEach(function (btn) {
btn.addEventListener('click', function () {
root.classList.remove('is-demo');
var i = Number(btn.getAttribute('data-i'));
root.setAttribute('data-active', String(i));
root.querySelector('.mt__fill').style.transform = 'scaleX(' + ((i + 1) / 4) + ')';
stops.forEach(function (s) { s.removeAttribute('aria-current'); });
btn.setAttribute('aria-current', 'step');
desc.textContent = milestoneNotes[i];
});
});import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./PressMilestoneTimeline.module.scss";
export type PressMilestoneTimelineProps = { duration?: number; easing?: string; color?: string };
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const STOPS = [
{ year: "2019", label: "창립", desc: "2019년, 세 명이 작은 사무실에서 첫 제품을 출시했습니다." },
{ year: "2021", label: "시리즈 A", desc: "2021년, 시리즈 A 투자를 유치하며 팀을 열 배로 키웠습니다." },
{ year: "2023", label: "해외 진출", desc: "2023년, 일본·미국 지사를 열고 해외 진출을 시작했습니다." },
{ year: "2026", label: "100만 사용자", desc: "2026년, 누적 가입자 100만 명을 넘겼습니다." },
];
export default function PressMilestoneTimeline({ duration = 300, easing = "cubic-bezier(.2,.8,.2,1)", color = "#2f6df6" }: PressMilestoneTimelineProps) {
const [active, setActive] = useState(0);
const [interacted, setInteracted] = useState(false);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={interacted ? styles.mt : `${styles.mt} ${styles.demo}`} style={vars}>
<p className={styles.desc} role="status">{STOPS[active].desc}</p>
<div className={styles.track}>
<span className={styles.fill} style={{ transform: `scaleX(${(active + 1) / STOPS.length})` }} aria-hidden="true" />
<ol className={styles.list}>
{STOPS.map((s, i) => (
<li key={s.year}>
<button
type="button"
className={styles.stop}
aria-current={i === active ? "step" : undefined}
onClick={() => { setInteracted(true); setActive(i); }}
>
<span className={styles.dot} aria-hidden="true" />
<span className={styles.year}>{s.year}</span>
<span className={styles.label}>{s.label}</span>
</button>
</li>
))}
</ol>
</div>
</section>
);
}08 · 沿革タイムライン — 計測 累積面積 0.438% · 動いたフレーム 7/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
説明文も一緒に変わる
上の説明文もその時点の内容に変わります。
stops.forEach(function (btn) {
btn.addEventListener('click', function () {
root.classList.remove('is-demo');
var i = Number(btn.getAttribute('data-i'));
root.setAttribute('data-active', String(i));
root.querySelector('.mt__fill').style.transform = 'scaleX(' + ((i + 1) / 4) + ')';
stops.forEach(function (s) { s.removeAttribute('aria-current'); });
btn.setAttribute('aria-current', 'step');
desc.textContent = milestoneNotes[i];
});
});
09プレスフッター
正規表現で本当に検証する
メールアドレスを入れて購読ボタンを押すと、実際に正規表現で形式を検証します。

<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 に同梱 · パスワードを見る
通過と失敗の合図
通過するとSVGチェックがopacityで現れ、失敗すると入力欄がtranslateXで左右に短く揺れます。
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 = '正しいメールアドレスを入力してください。';
}
});
どこで壊れるか — 落とし穴
不透明な背景が円を隠した
9つを作る中で実際に2度つまずきました。ひとつ目はメディア問い合わせ(06)です。最初はカード背景を不透明なクリーム色で塗っていましたが、背景に置いたドリフト円がz-index:-1のため、その不透明な背景の裏に完全に隠れてしまいました。見た目には何の問題もなさそうでしたが、実際に動くピクセルはほとんどなく、アニメーションと呼ぶには物足りない状態でした。カード背景をなくし、円が舞台の色の上に直接見えるようにしたことで解決しました。
box-sizing がないと
ふたつ目はプレスリリース一覧(02)とメディアキット(03)のレスポンシブでした。どちらもmax-heightで行を折りたたんだり開いたりしますが、box-sizingを指定しないとpaddingが指定した高さの上にそのまま加算され、実際のレンダー高さが大きくなります。幅320pxの画面ではこの差が積み重なり、気づかないうちに縦スクロールが発生していました。両方にbox-sizing: border-boxを加え、狭い画面用のパディングを減らして解決しました。
マーキーの間隔は margin-right
掲載メディア(04)のようにtranslateX(-50%)で無限スクロールするトラックにも、よくある落とし穴があります。項目同士の間隔をgapで取ると、2周目(2N個目)の継ぎ目だけgap ÷ 2分足りなくなり、一周するたびにわずかに跳ねます。このテンプレートは最初からmargin-rightで間隔を取ってこの問題を避けていて、7a6mw6ktで開くzipの中のコードも同じ方式のままです。上の二つの落とし穴を含めて、9つは表のとおり正確に動く最新の状態です。
数字の入った資料を画像だけで載せて検索に拾われないこの失敗は、ダッシュボードのグラフ操作で実データを扱うときに再び出会います。
アクセシビリティ
動きを止めても状態は残る
9つ全部がprefers-reduced-motion: reduceで装飾的な遷移とアニメーションを止めますが、状態そのものは残ります — 沿革の進捗バーは埋まったまま、フィルターした一覧は折りたたまれたまま固定され、数字ハイライトのカウントアップは情報なのでそのまま続きます。
ネイティブ操作と status
メディアキット(03)のチェックボックスはネイティブのinput[type="checkbox"]なので、Tab・Spaceキーでそのまま操作できます。メディア問い合わせ(06)とプレスフッター(09)の結果文はrole="status"(aria-live)なので、スクリーンリーダーがコピー・送信の結果をすぐに読み上げます。沿革タイムライン(08)はaria-current="step"で今見ている地点を伝え、創業者の言葉(07)のドットボタンはrole="group"とaria-selectedで状態を伝えます。
Clipboard API とフォールバック
クリップボードコピー(06)はMDN Clipboard APIの仕様に沿っていて、対応していない環境ではtextareaフォールバックに切り替わります。他の静的テンプレート素材はテンプレートカテゴリーに、このサイトが何を配っているかは紹介ページにまとめています。
FAQ
この プレスリリース テンプレート に実際の写真やロゴを使ってもいいですか?
はい、それが目的です。9つとも顔写真・ロゴの場所は図形のプレースホルダーなので、実際の画像やロゴに差し替えるだけで使えます。図形自体はdemo-tokens.scssの$subject-ink・$subject-blueの値を使っていて、舞台の背景色(イエロー・オレンジなど)は完全に別のトークンなので混ざる心配はありません。
React版はCSS版と動きが違いますか?
動作は同じです。選んだチップ・チェックボックスの状態・アクティブな引用文のような値はuseStateで持ち、変数3つ(duration・easing・color)はpropsで受け取ってCSS変数(--duration・--easing・--color)に渡します。数字ハイライト(05)のカウントアップはuseEffectの中で同じrequestAnimationFrameの処理をそのまま使います。
メディアキットのダウンロードは実際にファイルを受け取れますか?
チェックボックスの選択とボタンの有効化、完了表示はすべて実際の状態変化です。ただしデモ自体には接続された実ファイルがないため、クリックしても本当のダウンロードは始まりません。実際に使うときは各ファイル行に本物のhrefをつなぐだけで、チェック・カウントのロジックはそのまま再利用できます。記者ではなく顧客を説得する根拠が要るなら、信頼を作るLPパーツがその一式です。









