HUD デザインとは?画面に見つめ返される9パーツ
HUD デザインとは、ゲームやSF映画の照準器やレーダーのように、座標や状態の数値を画面に重ねて表示し、まるで自分が見つめ返されているような印象を与えるインターフェース手法です。
まずは下のワンページを実際にスクロールしてみてください。上部のナビゲーションから一番下のターミナル風フッターまで、4隅のブラケットとスキャンラインが画面全体で同じリズムで動いているかどうかが、このスタイルが成立しているかの判断基準です。

このワンページを作っている9個の部品は、ゲームの hud 風 デザイン をそのまま web に持ち込んだもので、下の格子でそれぞれ単独でも開けます。並び順は、訪問者が実際にページをたどる流れ(最初に目を引かれる、メニューを確認する、機能をざっと見る、進行ログを見る、料金を比べる、レビューで信頼する、もう一押しされる、登録する、フッターで締めくくる)そのままです。同じdesign柱で、琥珀色のブラケットではなくクロムの光沢を前面に出したのがy2k webデザインのパーツ、コントラストを限界まで下げて二重の影だけを残したのがニューモーフィズム デザインのLPキットです。
01 hero-hud
02 nav-hud
03 card-grid-hud
04 feature-list-hud
05 pricing-hud
06 testimonial-hud
07 cta-band-hud
08 footer-hud
09 buttons-inputs-hud
自動再生 · タイルを押すとその項目へ · すべて1つのzip
- 01 ロックオンヒーロー
- 02 ステータスバーナビ
- 03 ターゲット認識カード
- 04 システムログリスト
- 05 ロック対象プラン
- 06 顔認識風レビュー
- 07 アラート帯
- 08 ターミナルフッター
- 09 ロックオンボタン・スキャン入力欄
01ロックオンヒーロー
何が動くか
見出しカードを囲む4つの角ブラケットが0.15秒ずつずれたタイミングで内側へ狭まるように脈打ち、背後ではスキャンラインが上から下へ一度だけ流れます。

<main class="stage">
<section class="hero is-demo">
<span class="hero__corner hero__corner--tl" aria-hidden="true"></span>
<span class="hero__corner hero__corner--tr" aria-hidden="true"></span>
<span class="hero__corner hero__corner--bl" aria-hidden="true"></span>
<span class="hero__corner hero__corner--br" aria-hidden="true"></span>
<div class="hero__scan" aria-hidden="true"></div>
<p class="hero__eyebrow"><span class="hero__dot" aria-hidden="true"></span>SENTRY / ONLINE</p>
<h1 class="hero__title">見逃す瞬間なく、<br>見守るダッシュボード</h1>
<p class="hero__desc">リアルタイムで検知し、記録します。</p>
<button class="hero__cta" type="button">無料で始める</button>
<p class="hero__readout">SIG <span class="hero__cursor" aria-hidden="true"></span></p>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 브래킷 락온 + 스캔 한 주기
$easing: $ease-spring; // 브래킷이 조여드는 곡선
$color: $stage-yellow; // 호박색 터미널 강조색
@include stage($stage-ink);
.hero {
position: relative;
box-sizing: border-box;
width: clamp(240px, 82vw, 380px);
padding: $sp-5 $sp-4;
border-radius: $r-sm;
background: $subject-ink;
box-shadow: 0 0 0 1px rgba($color, .4), 0 0 24px rgba($color, .16);
overflow: hidden;
// 320px 폰 체크: eyebrow·2줄 제목·설명·CTA·터미널 readout 5단이 129·130 형제보다 한 단 더 많아
// 200px 높이를 넘는다 (131-01 실측: scrollHeight 238px vs innerHeight 200px). 단마다 여백을
// 한 단계씩 좁혀 맞춘다.
}
.hero__scan {
position: absolute;
inset: 0;
pointer-events: none;
background:
repeating-linear-gradient(to bottom, rgba($color, 0) 0, rgba($color, 0) 5px, rgba($color, .1) 6px, rgba($color, 0) 7px),
linear-gradient(to bottom, rgba($color, 0) 0%, rgba($color, .32) 48%, rgba($color, 0) 100%);
background-size: 100% 100%, 100% 240%;
background-position: 0 0, 0 -140%;
animation: scanSweep $duration linear infinite;
}
@keyframes scanSweep {
0% { background-position: 0 0, 0 -140%; }
100% { background-position: 0 0, 0 140%; }
}
.hero__corner {
position: absolute;
width: 18px;
height: 18px;
pointer-events: none;
border-color: $color;
border-style: solid;
border-width: 0;
animation: cornerPulse $duration $easing infinite;
}
.hero__corner--tl { top: 0; left: 0; border-top-width: 2px; border-left-width: 2px; transform-origin: top left; } // 규칙8: 괄호 꼭짓점 모서리 고정
.hero__corner--tr { top: 0; right: 0; border-top-width: 2px; border-right-width: 2px; transform-origin: top right; animation-delay: -.15s; } // 규칙8: 괄호 꼭짓점 모서리 고정
.hero__corner--bl { bottom: 0; left: 0; border-bottom-width: 2px; border-left-width: 2px; transform-origin: bottom left; animation-delay: -.3s; } // 규칙8: 괄호 꼭짓점 모서리 고정
.hero__corner--br { bottom: 0; right: 0; border-bottom-width: 2px; border-right-width: 2px; transform-origin: bottom right; animation-delay: -.45s; } // 규칙8: 괄호 꼭짓점 모서리 고정
@keyframes cornerPulse {
0%, 100% { transform: scale(.55); opacity: .35; }
50% { transform: scale(1); opacity: 1; }
}
.hero__eyebrow {
position: relative;
margin: 0 0 $sp-2;
display: flex;
align-items: center;
gap: $sp-2;
color: $color;
font-family: "Courier New", monospace;
font-weight: 700;
font-size: clamp(10px, 2.4vw, 11px);
line-height: 1;
letter-spacing: .12em;
text-transform: uppercase;
}
.hero__dot {
width: 6px;
height: 6px;
flex: none;
border-radius: 50%;
background: $color;
animation: blink $dur-loop steps(1) infinite;
}
@keyframes blink {
0%, 49% { opacity: 1; }
50%, 100% { opacity: .2; }
}
.hero__title {
position: relative;
margin: 0 0 $sp-1;
color: $subject-cream;
font-weight: 700;
font-size: clamp(18px, 5vw, 25px);
line-height: 1.15;
font-family: inherit;
}
.hero__desc {
position: relative;
margin: 0 0 $sp-3;
color: rgba($subject-cream, .85); // 스캔라인이 지나는 밝은 프레임(#544623)에서 .6 은 4.29:1 — .85 로 6.77:1
font-weight: 500;
font-size: clamp(12px, 2.8vw, 14px);
line-height: 1.4;
font-family: inherit;
}
.hero__cta {
position: relative;
border: 1px solid $color;
border-radius: $r-control;
background: rgba($color, .12);
color: $color;
padding: $sp-1 $sp-5;
font-weight: 700;
font-size: clamp(12px, 2.8vw, 13px);
line-height: 1;
font-family: inherit;
letter-spacing: .02em;
}
.hero__readout {
position: relative;
margin: $sp-2 0 0;
color: rgba($color, .7);
font-family: "Courier New", monospace;
font-weight: 400;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
letter-spacing: .04em;
}
.hero__cursor {
display: inline-block;
width: 6px;
height: 10px;
background: $color;
vertical-align: -1px;
animation: blink $dur-loop steps(1) infinite;
}
@media (prefers-reduced-motion: reduce) {
.hero__scan, .hero__corner, .hero__dot, .hero__cursor {
animation: none !important;
}
.hero__corner { transform: scale(1); opacity: 1; }
.hero__dot, .hero__cursor { 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);
}
.hero {
position: relative;
box-sizing: border-box;
width: clamp(240px, 82vw, 380px);
padding: 20px 16px;
border-radius: 8px;
background: #17141a;
box-shadow: 0 0 0 1px rgba(255, 210, 63, 0.4), 0 0 24px rgba(255, 210, 63, 0.16);
overflow: hidden;
}
.hero__scan {
position: absolute;
inset: 0;
pointer-events: none;
background: repeating-linear-gradient(to bottom, rgba(255, 210, 63, 0) 0, rgba(255, 210, 63, 0) 5px, rgba(255, 210, 63, 0.1) 6px, rgba(255, 210, 63, 0) 7px), linear-gradient(to bottom, rgba(255, 210, 63, 0) 0%, rgba(255, 210, 63, 0.32) 48%, rgba(255, 210, 63, 0) 100%);
background-size: 100% 100%, 100% 240%;
background-position: 0 0, 0 -140%;
animation: scanSweep 2s linear infinite;
}
@keyframes scanSweep {
0% {
background-position: 0 0, 0 -140%;
}
100% {
background-position: 0 0, 0 140%;
}
}
.hero__corner {
position: absolute;
width: 18px;
height: 18px;
pointer-events: none;
border-color: #ffd23f;
border-style: solid;
border-width: 0;
animation: cornerPulse 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.hero__corner--tl {
top: 0;
left: 0;
border-top-width: 2px;
border-left-width: 2px;
transform-origin: top left;
}
.hero__corner--tr {
top: 0;
right: 0;
border-top-width: 2px;
border-right-width: 2px;
transform-origin: top right;
animation-delay: -0.15s;
}
.hero__corner--bl {
bottom: 0;
left: 0;
border-bottom-width: 2px;
border-left-width: 2px;
transform-origin: bottom left;
animation-delay: -0.3s;
}
.hero__corner--br {
bottom: 0;
right: 0;
border-bottom-width: 2px;
border-right-width: 2px;
transform-origin: bottom right;
animation-delay: -0.45s;
}
@keyframes cornerPulse {
0%, 100% {
transform: scale(0.55);
opacity: 0.35;
}
50% {
transform: scale(1);
opacity: 1;
}
}
.hero__eyebrow {
position: relative;
margin: 0 0 8px;
display: flex;
align-items: center;
gap: 8px;
color: #ffd23f;
font-family: "Courier New", monospace;
font-weight: 700;
font-size: clamp(10px, 2.4vw, 11px);
line-height: 1;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.hero__dot {
width: 6px;
height: 6px;
flex: none;
border-radius: 50%;
background: #ffd23f;
animation: blink 2s steps(1) infinite;
}
@keyframes blink {
0%, 49% {
opacity: 1;
}
50%, 100% {
opacity: 0.2;
}
}
.hero__title {
position: relative;
margin: 0 0 4px;
color: #fff7e6;
font-weight: 700;
font-size: clamp(18px, 5vw, 25px);
line-height: 1.15;
font-family: inherit;
}
.hero__desc {
position: relative;
margin: 0 0 12px;
color: rgba(255, 247, 230, 0.85);
font-weight: 500;
font-size: clamp(12px, 2.8vw, 14px);
line-height: 1.4;
font-family: inherit;
}
.hero__cta {
position: relative;
border: 1px solid #ffd23f;
border-radius: 12px;
background: rgba(255, 210, 63, 0.12);
color: #ffd23f;
padding: 4px 20px;
font-weight: 700;
font-size: clamp(12px, 2.8vw, 13px);
line-height: 1;
font-family: inherit;
letter-spacing: 0.02em;
}
.hero__readout {
position: relative;
margin: 8px 0 0;
color: rgba(255, 210, 63, 0.7);
font-family: "Courier New", monospace;
font-weight: 400;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
letter-spacing: 0.04em;
}
.hero__cursor {
display: inline-block;
width: 6px;
height: 10px;
background: #ffd23f;
vertical-align: -1px;
animation: blink 2s steps(1) infinite;
}
@media (prefers-reduced-motion: reduce) {
.hero__scan, .hero__corner, .hero__dot, .hero__cursor {
animation: none !important;
}
.hero__corner {
transform: scale(1);
opacity: 1;
}
.hero__dot, .hero__cursor {
opacity: 1;
}
}import type { CSSProperties } from "react";
import styles from "./HeroHud.module.scss";
export type HeroHudProps = {
brand?: string;
title?: string;
desc?: string;
cta?: string;
readout?: string;
/** 한 주기(기본 2000ms) */
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function HeroHud({
brand = "SENTRY / ONLINE",
title = "놓치는 순간 없이 지켜보는 대시보드",
desc = "실시간으로 감지하고 기록합니다",
cta = "무료로 시작",
readout = "SIG",
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#ffd23f",
}: HeroHudProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.hero} style={vars}>
<span className={`${styles.corner} ${styles.cornerTl}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerTr}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerBl}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerBr}`} aria-hidden="true" />
<div className={styles.scan} aria-hidden="true" />
<p className={styles.eyebrow}>
<span className={styles.dot} aria-hidden="true" />
{brand}
</p>
<h1 className={styles.title}>{title}</h1>
<p className={styles.desc}>{desc}</p>
<button type="button" className={styles.cta}>{cta}</button>
<p className={styles.readoutLine}>
{readout} <span className={styles.cursor} aria-hidden="true" />
</p>
</section>
);
}01 · ロックオンヒーロー — 計測 累積面積 48.094% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
コードで見る所
SaaSランディングの最初の画面や新製品発表のカバーなど、「この画面が自分を認識している」という第一印象を作りたい場所に向いています。
<div class="relative w-full max-w-md overflow-hidden rounded-control bg-ink px-6 py-8 shadow-glow">
<span class="corner corner-tl" aria-hidden="true"></span>
<span class="corner corner-tr" aria-hidden="true"></span>
<span class="corner corner-bl" aria-hidden="true"></span>
<span class="corner corner-br" aria-hidden="true"></span>
<div class="scan-sweep absolute inset-0 pointer-events-none" aria-hidden="true"></div>
<p class="mb-3 flex items-center gap-2 text-[11px] font-bold uppercase tracking-widest text-accent">
<span class="blink-dot inline-block h-1.5 w-1.5 rounded-full bg-accent"></span>SENTRY / ONLINE
</p>
<h1 class="mb-2 text-2xl font-bold leading-tight">見逃す瞬間なく、見守るダッシュボード</h1>
<p class="mb-4 text-sm text-cream/60">リアルタイムで検知し、記録します。</p>
<button class="rounded-control border border-accent bg-accent/10 px-5 py-2 text-sm font-bold text-accent">無料で始める</button>
</div>
02ステータスバーナビ
何が動くか
ナビ全体がHUDのステータスバーのようにブラケットで囲まれ、アクティブなタブの下の線がスキャンするようにタブの間を滑って移動します。

<main class="stage">
<nav class="navhud is-demo" aria-label="SENTRY">
<span class="navhud__brand">SENTRY</span>
<ul class="navhud__track">
<li class="navhud__item">ホーム</li>
<li class="navhud__item">機能</li>
<li class="navhud__item">料金</li>
<li class="navhud__item">お問い合わせ</li>
<span class="navhud__indicator" aria-hidden="true"></span>
</ul>
<span class="navhud__status"><span class="navhud__dot" aria-hidden="true"></span>LIVE</span>
</nav>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 밑줄 인디케이터가 탭 4곳을 도는 한 주기
$easing: $ease-spring; // 인디케이터가 미끄러지는 곡선
$color: $stage-yellow; // 호박색 터미널 강조색
@include stage($stage-yellow);
.navhud {
position: relative;
box-sizing: border-box;
display: flex;
align-items: center;
gap: $sp-3;
width: clamp(260px, 84vw, 420px);
padding: $sp-2 $sp-3;
border-radius: $r-control;
background: $subject-ink;
box-shadow: 0 0 0 1px rgba($color, .4), 0 0 24px rgba($color, .16);
}
.navhud__brand {
flex: none;
color: $subject-cream;
font-weight: 800;
font-size: clamp(12px, 3vw, 13px);
line-height: 1;
font-family: inherit;
letter-spacing: .04em;
white-space: nowrap;
}
.navhud__track {
position: relative;
flex: 1 1 auto;
min-width: 0;
display: grid;
grid-template-columns: repeat(4, 1fr);
margin: 0;
padding: 0;
list-style: none;
}
.navhud__item {
padding: $sp-1 0;
color: rgba($subject-cream, .55);
font-weight: 600;
font-size: clamp(10px, 2.6vw, 11px);
line-height: 1;
font-family: inherit;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.navhud__indicator {
position: absolute;
left: 0;
bottom: 0;
width: 25%;
height: 2px;
background: $color;
animation: navScan $duration $easing infinite;
}
@keyframes navScan {
0%, 20% { transform: translateX(0); }
25%, 45% { transform: translateX(100%); }
50%, 70% { transform: translateX(200%); }
75%, 95% { transform: translateX(300%); }
100% { transform: translateX(0); }
}
.navhud__status {
flex: none;
display: flex;
align-items: center;
gap: $sp-1;
color: $color;
font-family: "Courier New", monospace;
font-weight: 700;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
letter-spacing: .08em;
white-space: nowrap;
}
.navhud__dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: $color;
animation: blink $dur-loop steps(1) infinite;
}
@keyframes blink {
0%, 49% { opacity: 1; }
50%, 100% { opacity: .2; }
}
@media (prefers-reduced-motion: reduce) {
.navhud__indicator, .navhud__dot {
animation: none !important;
}
.navhud__indicator { transform: translateX(0); }
.navhud__dot { opacity: 1; }
}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);
}
.navhud {
position: relative;
box-sizing: border-box;
display: flex;
align-items: center;
gap: 12px;
width: clamp(260px, 84vw, 420px);
padding: 8px 12px;
border-radius: 12px;
background: #17141a;
box-shadow: 0 0 0 1px rgba(255, 210, 63, 0.4), 0 0 24px rgba(255, 210, 63, 0.16);
}
.navhud__brand {
flex: none;
color: #fff7e6;
font-weight: 800;
font-size: clamp(12px, 3vw, 13px);
line-height: 1;
font-family: inherit;
letter-spacing: 0.04em;
white-space: nowrap;
}
.navhud__track {
position: relative;
flex: 1 1 auto;
min-width: 0;
display: grid;
grid-template-columns: repeat(4, 1fr);
margin: 0;
padding: 0;
list-style: none;
}
.navhud__item {
padding: 4px 0;
color: rgba(255, 247, 230, 0.55);
font-weight: 600;
font-size: clamp(10px, 2.6vw, 11px);
line-height: 1;
font-family: inherit;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.navhud__indicator {
position: absolute;
left: 0;
bottom: 0;
width: 25%;
height: 2px;
background: #ffd23f;
animation: navScan 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes navScan {
0%, 20% {
transform: translateX(0);
}
25%, 45% {
transform: translateX(100%);
}
50%, 70% {
transform: translateX(200%);
}
75%, 95% {
transform: translateX(300%);
}
100% {
transform: translateX(0);
}
}
.navhud__status {
flex: none;
display: flex;
align-items: center;
gap: 4px;
color: #ffd23f;
font-family: "Courier New", monospace;
font-weight: 700;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
letter-spacing: 0.08em;
white-space: nowrap;
}
.navhud__dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #ffd23f;
animation: blink 2s steps(1) infinite;
}
@keyframes blink {
0%, 49% {
opacity: 1;
}
50%, 100% {
opacity: 0.2;
}
}
@media (prefers-reduced-motion: reduce) {
.navhud__indicator, .navhud__dot {
animation: none !important;
}
.navhud__indicator {
transform: translateX(0);
}
.navhud__dot {
opacity: 1;
}
}import type { CSSProperties } from "react";
import styles from "./NavHud.module.scss";
export type NavHudProps = {
brand?: string;
links?: string[];
status?: string;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function NavHud({
brand = "SENTRY",
links = ["홈", "기능", "요금", "문의"],
status = "LIVE",
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#ffd23f",
}: NavHudProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<nav className={styles.nav} style={vars} aria-label={brand}>
<span className={styles.brand}>{brand}</span>
<ul className={styles.track}>
{links.map((label) => (
<li key={label} className={styles.item}>{label}</li>
))}
<span className={styles.indicator} aria-hidden="true" />
</ul>
<span className={styles.status}>
<span className={styles.dot} aria-hidden="true" />
{status}
</span>
</nav>
);
}02 · ステータスバーナビ — 計測 累積面積 0.389% · 動いたフレーム 7/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
コードで見る所
LP上部バーやダッシュボードナビのように、ブランド名・メニュー・状態表示を1行に同居させたい場所で使います。
<nav class="relative flex items-center gap-4 rounded-control bg-ink/95 px-3 py-2 shadow-glow" aria-label="SENTRY">
<span class="shrink-0 whitespace-nowrap pl-2 font-extrabold text-sm">SENTRY</span>
<div class="relative flex min-w-0 flex-1 items-stretch">
<span class="relative z-10 flex-1 whitespace-nowrap py-2 text-center text-xs font-semibold text-cream/70">ホーム</span>
<span class="relative z-10 flex-1 whitespace-nowrap py-2 text-center text-xs font-semibold text-cream/70">機能</span>
<span class="relative z-10 flex-1 whitespace-nowrap py-2 text-center text-xs font-semibold text-cream/70">料金</span>
<span class="relative z-10 flex-1 whitespace-nowrap py-2 text-center text-xs font-semibold text-cream/70">お問い合わせ</span>
<span class="nav-indicator absolute bottom-0 left-0 h-[2px] w-1/4 bg-accent"></span>
</div>
<span class="shrink-0 whitespace-nowrap flex items-center gap-1 pr-2 text-[10px] font-bold tracking-widest text-accent">
<span class="blink-dot inline-block h-1.5 w-1.5 rounded-full bg-accent"></span>LIVE
</span>
</nav>
03ターゲット認識カード
何が動くか
グリッドの各カードにカメラのビューファインダーのようなブラケット枠が付き、スキャンラインがカードを順番に一枚ずつ走査し、右上の座標表示も一緒に動きます。

<main class="stage">
<div class="cardgrid is-demo">
<article class="cardgrid__card">
<span class="cardgrid__corner cardgrid__corner--tl" aria-hidden="true"></span>
<span class="cardgrid__corner cardgrid__corner--tr" aria-hidden="true"></span>
<span class="cardgrid__corner cardgrid__corner--bl" aria-hidden="true"></span>
<span class="cardgrid__corner cardgrid__corner--br" aria-hidden="true"></span>
<div class="cardgrid__scan" aria-hidden="true"></div>
<span class="cardgrid__coord">04.12</span>
<span class="cardgrid__icon" aria-hidden="true"></span>
<p class="cardgrid__label">検知</p>
</article>
<article class="cardgrid__card">
<span class="cardgrid__corner cardgrid__corner--tl" aria-hidden="true"></span>
<span class="cardgrid__corner cardgrid__corner--tr" aria-hidden="true"></span>
<span class="cardgrid__corner cardgrid__corner--bl" aria-hidden="true"></span>
<span class="cardgrid__corner cardgrid__corner--br" aria-hidden="true"></span>
<div class="cardgrid__scan" aria-hidden="true"></div>
<span class="cardgrid__coord cardgrid__coord--alt">#A1</span>
<span class="cardgrid__icon" aria-hidden="true"></span>
<p class="cardgrid__label">追跡</p>
</article>
<article class="cardgrid__card">
<span class="cardgrid__corner cardgrid__corner--tl" aria-hidden="true"></span>
<span class="cardgrid__corner cardgrid__corner--tr" aria-hidden="true"></span>
<span class="cardgrid__corner cardgrid__corner--bl" aria-hidden="true"></span>
<span class="cardgrid__corner cardgrid__corner--br" aria-hidden="true"></span>
<div class="cardgrid__scan" aria-hidden="true"></div>
<span class="cardgrid__coord">07.03</span>
<span class="cardgrid__icon" aria-hidden="true"></span>
<p class="cardgrid__label">記録</p>
</article>
<article class="cardgrid__card">
<span class="cardgrid__corner cardgrid__corner--tl" aria-hidden="true"></span>
<span class="cardgrid__corner cardgrid__corner--tr" aria-hidden="true"></span>
<span class="cardgrid__corner cardgrid__corner--bl" aria-hidden="true"></span>
<span class="cardgrid__corner cardgrid__corner--br" aria-hidden="true"></span>
<div class="cardgrid__scan" aria-hidden="true"></div>
<span class="cardgrid__coord cardgrid__coord--alt">#C4</span>
<span class="cardgrid__icon" aria-hidden="true"></span>
<p class="cardgrid__label">分析</p>
</article>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 스캔이 카드 4장을 순서대로 도는 한 주기
$easing: $ease-spring; // 브래킷이 조여드는 곡선
$color: $stage-yellow; // 호박색 터미널 강조색
@include stage($stage-orange);
.cardgrid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: $sp-2;
width: clamp(240px, 84vw, 340px);
}
// 320px 폰 체크: 2x2 고정 그리드라 폭이 줄어도 항상 2행 — coord 줄(HUD 전용 읽음값)까지 4줄이라
// 카드 padding·행간이 129·130 형제와 같은 값이면 2행 합계가 200px 높이를 넘는다
// (131-03 실측: scrollHeight 239px vs innerHeight 200px). padding·margin 을 한 단계씩 좁힌다.
.cardgrid__card {
position: relative;
box-sizing: border-box;
padding: $sp-2;
border-radius: $r-card;
background: $subject-ink;
box-shadow: 0 0 0 1px rgba($color, .4), 0 0 24px rgba($color, .16);
overflow: hidden;
}
.cardgrid__card:nth-child(1) .cardgrid__scan { animation-delay: 0s; }
.cardgrid__card:nth-child(2) .cardgrid__scan { animation-delay: -.5s; }
.cardgrid__card:nth-child(3) .cardgrid__scan { animation-delay: -1s; }
.cardgrid__card:nth-child(4) .cardgrid__scan { animation-delay: -1.5s; }
.cardgrid__scan {
position: absolute;
inset: 0;
pointer-events: none;
background: linear-gradient(to bottom, rgba($color, 0) 0%, rgba($color, .3) 48%, rgba($color, 0) 100%);
background-size: 100% 240%;
background-position: 0 -140%;
animation: cardScan $duration linear infinite;
}
@keyframes cardScan {
0% { background-position: 0 -140%; }
100% { background-position: 0 140%; }
}
.cardgrid__corner {
position: absolute;
width: 12px;
height: 12px;
pointer-events: none;
border-color: $color;
border-style: solid;
border-width: 0;
animation: cornerPulse $duration $easing infinite;
}
.cardgrid__corner--tl { top: $sp-2; left: $sp-2; border-top-width: 2px; border-left-width: 2px; transform-origin: top left; } // 규칙8: 괄호 꼭짓점 모서리 고정
.cardgrid__corner--tr { top: $sp-2; right: $sp-2; border-top-width: 2px; border-right-width: 2px; transform-origin: top right; animation-delay: -.15s; } // 규칙8: 괄호 꼭짓점 모서리 고정
.cardgrid__corner--bl { bottom: $sp-2; left: $sp-2; border-bottom-width: 2px; border-left-width: 2px; transform-origin: bottom left; animation-delay: -.3s; } // 규칙8: 괄호 꼭짓점 모서리 고정
.cardgrid__corner--br { bottom: $sp-2; right: $sp-2; border-bottom-width: 2px; border-right-width: 2px; transform-origin: bottom right; animation-delay: -.45s; } // 규칙8: 괄호 꼭짓점 모서리 고정
@keyframes cornerPulse {
0%, 100% { transform: scale(.55); opacity: .35; }
50% { transform: scale(1); opacity: 1; }
}
.cardgrid__coord {
position: relative;
display: block;
margin: 0 0 $sp-1;
color: rgba($color, .85); // 스캔라인이 지나는 밝은 프레임(#4e4123)에서 .7 은 4.30:1 — .85 로 5.49:1
font-family: "Courier New", monospace;
font-weight: 700;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
letter-spacing: .04em;
text-align: right;
}
.cardgrid__coord--alt {
color: $subject-sky; // 어두운 카드 면 위 쿨 강조 글자. 이 카드는 호박색 스캔라인이 늘 지나 면이 #352c1f 까지 밝아진다 — $subject-blue 2.59:1, $subject-blue-light 도 그 프레임에서 4.28:1 로 미달이라 한 단계 더 밝은 $subject-sky(7.42:1)
}
.cardgrid__icon {
position: relative;
display: block;
width: 20px;
height: 20px;
margin: 0 0 $sp-2;
border: 1px solid $color;
border-radius: $r-xs;
background: rgba($color, .18);
}
.cardgrid__label {
position: relative;
margin: 0;
color: $subject-cream;
font-weight: 700;
font-size: clamp(12px, 2.8vw, 13px);
line-height: 1.3;
font-family: inherit;
}
@media (prefers-reduced-motion: reduce) {
.cardgrid__scan, .cardgrid__corner {
animation: none !important;
}
.cardgrid__corner { transform: scale(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: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.cardgrid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
width: clamp(240px, 84vw, 340px);
}
.cardgrid__card {
position: relative;
box-sizing: border-box;
padding: 8px;
border-radius: 16px;
background: #17141a;
box-shadow: 0 0 0 1px rgba(255, 210, 63, 0.4), 0 0 24px rgba(255, 210, 63, 0.16);
overflow: hidden;
}
.cardgrid__card:nth-child(1) .cardgrid__scan {
animation-delay: 0s;
}
.cardgrid__card:nth-child(2) .cardgrid__scan {
animation-delay: -0.5s;
}
.cardgrid__card:nth-child(3) .cardgrid__scan {
animation-delay: -1s;
}
.cardgrid__card:nth-child(4) .cardgrid__scan {
animation-delay: -1.5s;
}
.cardgrid__scan {
position: absolute;
inset: 0;
pointer-events: none;
background: linear-gradient(to bottom, rgba(255, 210, 63, 0) 0%, rgba(255, 210, 63, 0.3) 48%, rgba(255, 210, 63, 0) 100%);
background-size: 100% 240%;
background-position: 0 -140%;
animation: cardScan 2s linear infinite;
}
@keyframes cardScan {
0% {
background-position: 0 -140%;
}
100% {
background-position: 0 140%;
}
}
.cardgrid__corner {
position: absolute;
width: 12px;
height: 12px;
pointer-events: none;
border-color: #ffd23f;
border-style: solid;
border-width: 0;
animation: cornerPulse 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.cardgrid__corner--tl {
top: 8px;
left: 8px;
border-top-width: 2px;
border-left-width: 2px;
transform-origin: top left;
}
.cardgrid__corner--tr {
top: 8px;
right: 8px;
border-top-width: 2px;
border-right-width: 2px;
transform-origin: top right;
animation-delay: -0.15s;
}
.cardgrid__corner--bl {
bottom: 8px;
left: 8px;
border-bottom-width: 2px;
border-left-width: 2px;
transform-origin: bottom left;
animation-delay: -0.3s;
}
.cardgrid__corner--br {
bottom: 8px;
right: 8px;
border-bottom-width: 2px;
border-right-width: 2px;
transform-origin: bottom right;
animation-delay: -0.45s;
}
@keyframes cornerPulse {
0%, 100% {
transform: scale(0.55);
opacity: 0.35;
}
50% {
transform: scale(1);
opacity: 1;
}
}
.cardgrid__coord {
position: relative;
display: block;
margin: 0 0 4px;
color: rgba(255, 210, 63, 0.85);
font-family: "Courier New", monospace;
font-weight: 700;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
letter-spacing: 0.04em;
text-align: right;
}
.cardgrid__coord--alt {
color: #6ec8ff;
}
.cardgrid__icon {
position: relative;
display: block;
width: 20px;
height: 20px;
margin: 0 0 8px;
border: 1px solid #ffd23f;
border-radius: 4px;
background: rgba(255, 210, 63, 0.18);
}
.cardgrid__label {
position: relative;
margin: 0;
color: #fff7e6;
font-weight: 700;
font-size: clamp(12px, 2.8vw, 13px);
line-height: 1.3;
font-family: inherit;
}
@media (prefers-reduced-motion: reduce) {
.cardgrid__scan, .cardgrid__corner {
animation: none !important;
}
.cardgrid__corner {
transform: scale(1);
opacity: 1;
}
}import type { CSSProperties } from "react";
import styles from "./CardGridHud.module.scss";
export type CardGridHudCard = { coord: string; label: string; alt?: boolean };
export type CardGridHudProps = {
cards?: CardGridHudCard[];
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const defaultCards: CardGridHudCard[] = [
{ coord: "04.12", label: "감지" },
{ coord: "#A1", label: "추적", alt: true },
{ coord: "07.03", label: "기록" },
{ coord: "#C4", label: "분석", alt: true },
];
export default function CardGridHud({
cards = defaultCards,
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#ffd23f",
}: CardGridHudProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.grid} style={vars}>
{cards.map((card) => (
<article key={card.coord} className={styles.card}>
<span className={`${styles.corner} ${styles.cornerTl}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerTr}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerBl}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerBr}`} aria-hidden="true" />
<div className={styles.scan} aria-hidden="true" />
<span className={`${styles.coord} ${card.alt ? styles.coordAlt : ""}`}>{card.coord}</span>
<span className={styles.icon} aria-hidden="true" />
<p className={styles.label}>{card.label}</p>
</article>
))}
</div>
);
}03 · ターゲット認識カード — 計測 累積面積 34.317% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
コードで見る所
機能紹介グリッドやポートフォリオカードのように、同じ形のカードが何枚も並ぶ場所に向いています。
<div class="relative overflow-hidden rounded-card bg-ink p-4 shadow-glow">
<span class="corner corner-sm corner-tl" aria-hidden="true"></span>
<span class="corner corner-sm corner-tr" aria-hidden="true"></span>
<span class="corner corner-sm corner-bl" aria-hidden="true"></span>
<span class="corner corner-sm corner-br" aria-hidden="true"></span>
<div class="scan-sweep absolute inset-0 pointer-events-none" style="animation-delay:-.5s"></div>
<span class="relative mb-2 block text-right text-[10px] font-bold text-data/85">#A1</span>
<span class="relative mb-3 block h-5 w-5 rounded border border-accent bg-accent/20"></span>
<p class="relative text-sm font-bold">追跡</p>
</div>
04システムログリスト
何が動くか
リストの各行にタイムスタンプが付き、明るいスキャンバーが上から下へ一行ずつ走査してOK表示を残していきます。

<main class="stage">
<div class="loglist is-demo">
<ul class="loglist__rows">
<li class="loglist__row"><span class="loglist__ts">00:01</span><span class="loglist__text">ユーザー認証を確認</span><span class="loglist__ok">OK</span></li>
<li class="loglist__row"><span class="loglist__ts">00:02</span><span class="loglist__text">位置データを同期</span><span class="loglist__ok">OK</span></li>
<li class="loglist__row"><span class="loglist__ts">00:03</span><span class="loglist__text">異常パターンをスキャン</span><span class="loglist__ok">OK</span></li>
<li class="loglist__row"><span class="loglist__ts">00:04</span><span class="loglist__text">バックアップサーバーに接続</span><span class="loglist__ok">OK</span></li>
</ul>
<p class="loglist__cursorline">READY <span class="loglist__cursor" aria-hidden="true"></span></p>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 스캔 바가 줄 4개를 순서대로 내려가는 한 주기
$easing: $ease-spring; // 스캔 바가 밝아지는 곡선
$color: $stage-yellow; // 호박색 터미널 강조색
@include stage($stage-paper);
.loglist {
box-sizing: border-box;
width: clamp(240px, 84vw, 360px);
padding: $sp-4;
border-radius: $r-card;
background: $subject-ink;
box-shadow: 0 0 0 1px rgba($color, .4), 0 0 24px rgba($color, .16);
// 320px 폰 체크: 로그 4줄 + 커서줄이 129·130 형제 값 그대로면 200px 높이를 넘는다
// (131-04 실측: scrollHeight 223px vs innerHeight 200px). padding·행간을 한 단계씩 좁힌다.
}
.loglist__rows {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: $sp-1;
}
.loglist__row {
position: relative;
display: flex;
align-items: center;
gap: $sp-3;
padding: $sp-1 $sp-3;
border-radius: $r-xs;
animation: rowScan $duration $easing infinite;
}
.loglist__row:nth-child(1) { animation-delay: 0s; }
.loglist__row:nth-child(2) { animation-delay: -.5s; }
.loglist__row:nth-child(3) { animation-delay: -1s; }
.loglist__row:nth-child(4) { animation-delay: -1.5s; }
@keyframes rowScan {
0%, 100% { box-shadow: inset 0 0 0 1px rgba($color, 0), inset 0 0 0 0 rgba($color, 0); }
5% { box-shadow: inset 0 0 0 1px rgba($color, .5), inset 0 0 16px 0 rgba($color, .18); }
15% { box-shadow: inset 0 0 0 1px rgba($color, 0), inset 0 0 0 0 rgba($color, 0); }
}
.loglist__ts {
flex: none;
color: rgba($color, .7);
font-family: "Courier New", monospace;
font-weight: 700;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
}
.loglist__text {
flex: 1 1 auto;
min-width: 0;
color: $subject-cream;
font-weight: 600;
font-size: clamp(11px, 2.6vw, 12px);
line-height: 1.3;
font-family: inherit;
}
.loglist__ok {
flex: none;
opacity: 0;
color: $color;
font-family: "Courier New", monospace;
font-weight: 700;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
letter-spacing: .06em;
animation: rowOk $duration $easing infinite;
}
.loglist__row:nth-child(1) .loglist__ok { animation-delay: 0s; }
.loglist__row:nth-child(2) .loglist__ok { animation-delay: -.5s; }
.loglist__row:nth-child(3) .loglist__ok { animation-delay: -1s; }
.loglist__row:nth-child(4) .loglist__ok { animation-delay: -1.5s; }
@keyframes rowOk {
0%, 8% { opacity: 0; }
14%, 100% { opacity: 1; }
}
.loglist__cursorline {
margin: $sp-2 0 0;
color: rgba($color, .7);
font-family: "Courier New", monospace;
font-weight: 400;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
letter-spacing: .04em;
}
.loglist__cursor {
display: inline-block;
width: 6px;
height: 10px;
background: $color;
vertical-align: -1px;
animation: blink $dur-loop steps(1) infinite;
}
@keyframes blink {
0%, 49% { opacity: 1; }
50%, 100% { opacity: .2; }
}
@media (prefers-reduced-motion: reduce) {
.loglist__row, .loglist__ok, .loglist__cursor {
animation: none !important;
}
.loglist__row { box-shadow: inset 0 0 0 1px rgba($color, 0); }
.loglist__ok { opacity: 1; }
.loglist__cursor { opacity: 1; }
}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);
}
.loglist {
box-sizing: border-box;
width: clamp(240px, 84vw, 360px);
padding: 16px;
border-radius: 16px;
background: #17141a;
box-shadow: 0 0 0 1px rgba(255, 210, 63, 0.4), 0 0 24px rgba(255, 210, 63, 0.16);
}
.loglist__rows {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 4px;
}
.loglist__row {
position: relative;
display: flex;
align-items: center;
gap: 12px;
padding: 4px 12px;
border-radius: 4px;
animation: rowScan 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.loglist__row:nth-child(1) {
animation-delay: 0s;
}
.loglist__row:nth-child(2) {
animation-delay: -0.5s;
}
.loglist__row:nth-child(3) {
animation-delay: -1s;
}
.loglist__row:nth-child(4) {
animation-delay: -1.5s;
}
@keyframes rowScan {
0%, 100% {
box-shadow: inset 0 0 0 1px rgba(255, 210, 63, 0), inset 0 0 0 0 rgba(255, 210, 63, 0);
}
5% {
box-shadow: inset 0 0 0 1px rgba(255, 210, 63, 0.5), inset 0 0 16px 0 rgba(255, 210, 63, 0.18);
}
15% {
box-shadow: inset 0 0 0 1px rgba(255, 210, 63, 0), inset 0 0 0 0 rgba(255, 210, 63, 0);
}
}
.loglist__ts {
flex: none;
color: rgba(255, 210, 63, 0.7);
font-family: "Courier New", monospace;
font-weight: 700;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
}
.loglist__text {
flex: 1 1 auto;
min-width: 0;
color: #fff7e6;
font-weight: 600;
font-size: clamp(11px, 2.6vw, 12px);
line-height: 1.3;
font-family: inherit;
}
.loglist__ok {
flex: none;
opacity: 0;
color: #ffd23f;
font-family: "Courier New", monospace;
font-weight: 700;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
letter-spacing: 0.06em;
animation: rowOk 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.loglist__row:nth-child(1) .loglist__ok {
animation-delay: 0s;
}
.loglist__row:nth-child(2) .loglist__ok {
animation-delay: -0.5s;
}
.loglist__row:nth-child(3) .loglist__ok {
animation-delay: -1s;
}
.loglist__row:nth-child(4) .loglist__ok {
animation-delay: -1.5s;
}
@keyframes rowOk {
0%, 8% {
opacity: 0;
}
14%, 100% {
opacity: 1;
}
}
.loglist__cursorline {
margin: 8px 0 0;
color: rgba(255, 210, 63, 0.7);
font-family: "Courier New", monospace;
font-weight: 400;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
letter-spacing: 0.04em;
}
.loglist__cursor {
display: inline-block;
width: 6px;
height: 10px;
background: #ffd23f;
vertical-align: -1px;
animation: blink 2s steps(1) infinite;
}
@keyframes blink {
0%, 49% {
opacity: 1;
}
50%, 100% {
opacity: 0.2;
}
}
@media (prefers-reduced-motion: reduce) {
.loglist__row, .loglist__ok, .loglist__cursor {
animation: none !important;
}
.loglist__row {
box-shadow: inset 0 0 0 1px rgba(255, 210, 63, 0);
}
.loglist__ok {
opacity: 1;
}
.loglist__cursor {
opacity: 1;
}
}import type { CSSProperties } from "react";
import styles from "./FeatureListHud.module.scss";
export type FeatureListHudRow = { ts: string; text: string };
export type FeatureListHudProps = {
rows?: FeatureListHudRow[];
ok?: string;
ready?: string;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const defaultRows: FeatureListHudRow[] = [
{ ts: "00:01", text: "사용자 인증 확인" },
{ ts: "00:02", text: "위치 데이터 동기화" },
{ ts: "00:03", text: "이상 패턴 스캔" },
{ ts: "00:04", text: "백업 서버 연결" },
];
export default function FeatureListHud({
rows = defaultRows,
ok = "OK",
ready = "READY",
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#ffd23f",
}: FeatureListHudProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.list} style={vars}>
<ul className={styles.rows}>
{rows.map((row) => (
<li key={row.ts} className={styles.row}>
<span className={styles.ts}>{row.ts}</span>
<span className={styles.text}>{row.text}</span>
<span className={styles.ok}>{ok}</span>
</li>
))}
</ul>
<p className={styles.cursorline}>
{ready} <span className={styles.cursor} aria-hidden="true" />
</p>
</div>
);
}04 · システムログリスト — 計測 累積面積 9.292% · 動いたフレーム 18/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
コードで見る所
機能一覧やオンボーディングのチェックリストのように、確認作業が順番に終わっていく感覚を伝えたい場所に使います。
<ul class="flex flex-col gap-1">
<li class="row-scan flex items-center gap-3 rounded px-3 py-2">
<span class="flex-none text-[10px] font-bold text-accent/70">00:01</span>
<span class="min-w-0 flex-1 text-sm font-semibold">ユーザー認証を確認</span>
<span class="flex-none text-[10px] font-bold text-accent">OK</span>
</li>
<li class="row-scan flex items-center gap-3 rounded px-3 py-2" style="animation-delay:-.5s">
<span class="flex-none text-[10px] font-bold text-accent/70">00:02</span>
<span class="min-w-0 flex-1 text-sm font-semibold">位置データを同期</span>
<span class="flex-none text-[10px] font-bold text-accent">OK</span>
</li>
</ul>
<p class="mt-3 text-[10px] text-accent/70">READY <span class="blink-dot inline-block h-2.5 w-1.5 bg-accent"></span></p>
05ロック対象プラン
何が動くか
おすすめプランのカードだけブラケットが呼吸するように脈打ってLOCKEDバッジが付き、もう一方のカードは不透明度を下げてピントが外れたように後ろへ下がります。

<main class="stage">
<div class="pricing is-demo">
<article class="pricing__card pricing__card--dim">
<p class="pricing__name">ライト</p>
<p class="pricing__price">無料</p>
<ul class="pricing__feats">
<li>ベーシックスキャン</li>
<li>チャンネル1つ</li>
</ul>
</article>
<article class="pricing__card pricing__card--locked">
<span class="pricing__badge">LOCKED</span>
<span class="pricing__corner pricing__corner--tl" aria-hidden="true"></span>
<span class="pricing__corner pricing__corner--tr" aria-hidden="true"></span>
<span class="pricing__corner pricing__corner--bl" aria-hidden="true"></span>
<span class="pricing__corner pricing__corner--br" aria-hidden="true"></span>
<p class="pricing__name">プロ</p>
<p class="pricing__price">月1,200円</p>
<ul class="pricing__feats">
<li>リアルタイム追跡</li>
<li>チャンネル無制限</li>
</ul>
</article>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 브래킷 맥동 + 글로우 호흡 한 주기
$easing: $ease-spring; // 잠금이 숨 쉬는 곡선
$color: $stage-yellow; // 호박색 터미널 강조색
@include stage($stage-ink);
.pricing {
display: flex;
align-items: flex-start;
gap: $sp-4;
width: clamp(260px, 84vw, 420px);
}
.pricing__card {
position: relative;
box-sizing: border-box;
min-width: 0;
// flex-basis 고정 160px + flex-wrap:wrap 조합은 320px 폰 체크에서 84vw(268.8px) 폭에
// 카드 2장(160+160+gap)이 안 들어가 줄바꿈되어 세로로 쌓인다 — 높이가 200px 을 훌쩍 넘겼다
// (131-05 실측: docEl.scrollHeight 311px vs innerHeight 200px). 129·130 형제 항목처럼
// wrap 없이 flex:1 로 균등 분배해 항상 한 줄에 나란히 놓는다.
flex: 1 1 0;
padding: $sp-5 $sp-4;
border-radius: $r-card;
background: $subject-ink;
box-shadow: 0 0 0 1px rgba($color, .4), 0 0 24px rgba($color, .16);
}
.pricing__card--dim {
opacity: .55;
}
.pricing__card--locked::before {
content: "";
position: absolute;
inset: -1px;
border-radius: inherit;
pointer-events: none;
box-shadow: 0 0 0 1px rgba($color, .6), 0 0 32px rgba($color, .4);
opacity: .4;
animation: glowBreathe $duration $easing infinite;
}
@keyframes glowBreathe {
0%, 100% { opacity: .4; }
50% { opacity: 1; }
}
.pricing__badge {
position: relative;
display: inline-block;
margin: 0 0 $sp-2;
padding: $sp-1 $sp-2;
border-radius: $r-xs;
background: rgba($color, .16);
color: $color;
font-family: "Courier New", monospace;
font-weight: 700;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
letter-spacing: .08em;
}
.pricing__corner {
position: absolute;
width: 16px;
height: 16px;
pointer-events: none;
border-color: $color;
border-style: solid;
border-width: 0;
animation: cornerPulse $duration $easing infinite;
}
.pricing__corner--tl { top: $sp-2; left: $sp-2; border-top-width: 2px; border-left-width: 2px; transform-origin: top left; } // 규칙8: 괄호 꼭짓점 모서리 고정
.pricing__corner--tr { top: $sp-2; right: $sp-2; border-top-width: 2px; border-right-width: 2px; transform-origin: top right; animation-delay: -.15s; } // 규칙8: 괄호 꼭짓점 모서리 고정
.pricing__corner--bl { bottom: $sp-2; left: $sp-2; border-bottom-width: 2px; border-left-width: 2px; transform-origin: bottom left; animation-delay: -.3s; } // 규칙8: 괄호 꼭짓점 모서리 고정
.pricing__corner--br { bottom: $sp-2; right: $sp-2; border-bottom-width: 2px; border-right-width: 2px; transform-origin: bottom right; animation-delay: -.45s; } // 규칙8: 괄호 꼭짓점 모서리 고정
@keyframes cornerPulse {
0%, 100% { transform: scale(.55); opacity: .35; }
50% { transform: scale(1); opacity: 1; }
}
.pricing__name {
position: relative;
margin: 0 0 $sp-1;
color: $subject-cream;
font-weight: 700;
font-size: clamp(13px, 3vw, 15px);
line-height: 1.2;
font-family: inherit;
}
.pricing__price {
position: relative;
margin: 0 0 $sp-3;
color: rgba($subject-cream, .6);
font-weight: 600;
font-size: clamp(11px, 2.6vw, 12px);
line-height: 1.2;
font-family: inherit;
}
// dim 카드는 .pricing__card--dim 의 opacity .55 가 글자 알파에 곱해진다 — 기본 .6/.75 는 실효 .33/.41 이라 2.89~3.82:1.
// dim 카드에서만 알파를 빼 실효 .55 (5.85:1) 로 올린다. 밝은 카드의 위계(.6/.75)는 그대로 둔다
.pricing__card--dim .pricing__price,
.pricing__card--dim .pricing__feats { color: $subject-cream; }
.pricing__feats {
position: relative;
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: $sp-1;
color: rgba($subject-cream, .75);
font-weight: 500;
font-size: clamp(10px, 2.4vw, 11px);
line-height: 1.5;
font-family: inherit;
}
@media (prefers-reduced-motion: reduce) {
.pricing__card--locked::before, .pricing__corner {
animation: none !important;
}
.pricing__card--locked::before { opacity: 1; }
.pricing__corner { transform: scale(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);
}
.pricing {
display: flex;
align-items: flex-start;
gap: 16px;
width: clamp(260px, 84vw, 420px);
}
.pricing__card {
position: relative;
box-sizing: border-box;
min-width: 0;
flex: 1 1 0;
padding: 20px 16px;
border-radius: 16px;
background: #17141a;
box-shadow: 0 0 0 1px rgba(255, 210, 63, 0.4), 0 0 24px rgba(255, 210, 63, 0.16);
}
.pricing__card--dim {
opacity: 0.55;
}
.pricing__card--locked::before {
content: "";
position: absolute;
inset: -1px;
border-radius: inherit;
pointer-events: none;
box-shadow: 0 0 0 1px rgba(255, 210, 63, 0.6), 0 0 32px rgba(255, 210, 63, 0.4);
opacity: 0.4;
animation: glowBreathe 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes glowBreathe {
0%, 100% {
opacity: 0.4;
}
50% {
opacity: 1;
}
}
.pricing__badge {
position: relative;
display: inline-block;
margin: 0 0 8px;
padding: 4px 8px;
border-radius: 4px;
background: rgba(255, 210, 63, 0.16);
color: #ffd23f;
font-family: "Courier New", monospace;
font-weight: 700;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
letter-spacing: 0.08em;
}
.pricing__corner {
position: absolute;
width: 16px;
height: 16px;
pointer-events: none;
border-color: #ffd23f;
border-style: solid;
border-width: 0;
animation: cornerPulse 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.pricing__corner--tl {
top: 8px;
left: 8px;
border-top-width: 2px;
border-left-width: 2px;
transform-origin: top left;
}
.pricing__corner--tr {
top: 8px;
right: 8px;
border-top-width: 2px;
border-right-width: 2px;
transform-origin: top right;
animation-delay: -0.15s;
}
.pricing__corner--bl {
bottom: 8px;
left: 8px;
border-bottom-width: 2px;
border-left-width: 2px;
transform-origin: bottom left;
animation-delay: -0.3s;
}
.pricing__corner--br {
bottom: 8px;
right: 8px;
border-bottom-width: 2px;
border-right-width: 2px;
transform-origin: bottom right;
animation-delay: -0.45s;
}
@keyframes cornerPulse {
0%, 100% {
transform: scale(0.55);
opacity: 0.35;
}
50% {
transform: scale(1);
opacity: 1;
}
}
.pricing__name {
position: relative;
margin: 0 0 4px;
color: #fff7e6;
font-weight: 700;
font-size: clamp(13px, 3vw, 15px);
line-height: 1.2;
font-family: inherit;
}
.pricing__price {
position: relative;
margin: 0 0 12px;
color: rgba(255, 247, 230, 0.6);
font-weight: 600;
font-size: clamp(11px, 2.6vw, 12px);
line-height: 1.2;
font-family: inherit;
}
.pricing__card--dim .pricing__price,
.pricing__card--dim .pricing__feats {
color: #fff7e6;
}
.pricing__feats {
position: relative;
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 4px;
color: rgba(255, 247, 230, 0.75);
font-weight: 500;
font-size: clamp(10px, 2.4vw, 11px);
line-height: 1.5;
font-family: inherit;
}
@media (prefers-reduced-motion: reduce) {
.pricing__card--locked::before, .pricing__corner {
animation: none !important;
}
.pricing__card--locked::before {
opacity: 1;
}
.pricing__corner {
transform: scale(1);
opacity: 1;
}
}import type { CSSProperties } from "react";
import styles from "./PricingHud.module.scss";
export type PricingHudProps = {
dimName?: string;
dimPrice?: string;
dimFeats?: string[];
lockedName?: string;
lockedPrice?: string;
lockedFeats?: string[];
badge?: string;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function PricingHud({
dimName = "라이트",
dimPrice = "무료",
dimFeats = ["기본 스캔", "채널 1개"],
lockedName = "프로",
lockedPrice = "월 9,900원",
lockedFeats = ["실시간 추적", "채널 무제한"],
badge = "LOCKED",
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#ffd23f",
}: PricingHudProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.pricing} style={vars}>
<article className={`${styles.card} ${styles.dim}`}>
<p className={styles.name}>{dimName}</p>
<p className={styles.price}>{dimPrice}</p>
<ul className={styles.feats}>
{dimFeats.map((f) => <li key={f}>{f}</li>)}
</ul>
</article>
<article className={`${styles.card} ${styles.locked}`}>
<span className={styles.badge}>{badge}</span>
<span className={`${styles.corner} ${styles.cornerTl}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerTr}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerBl}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerBr}`} aria-hidden="true" />
<p className={styles.name}>{lockedName}</p>
<p className={styles.price}>{lockedPrice}</p>
<ul className={styles.feats}>
{lockedFeats.map((f) => <li key={f}>{f}</li>)}
</ul>
</article>
</div>
);
}05 · ロック対象プラン — 計測 累積面積 5.45% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
コードで見る所
2段階の料金プランや無料・有料比較のように、どちらを推しているか一目で伝えたいときに使います。
<div class="glow-breathe relative flex-1 basis-40 rounded-card bg-ink p-5 shadow-glow">
<span class="corner corner-sm corner-tl" aria-hidden="true"></span>
<span class="corner corner-sm corner-tr" aria-hidden="true"></span>
<span class="corner corner-sm corner-bl" aria-hidden="true"></span>
<span class="corner corner-sm corner-br" aria-hidden="true"></span>
<span class="mb-2 inline-block rounded bg-accent/15 px-2 py-1 text-[10px] font-bold tracking-widest text-accent">LOCKED</span>
<p class="mb-1 text-base font-bold">プロ</p>
<p class="mb-3 text-xs text-cream/60">月1,200円</p>
<ul class="flex flex-col gap-1 text-xs text-cream/75">
<li>リアルタイム追跡</li>
<li>チャンネル無制限</li>
</ul>
</div>
06顔認識風レビュー
何が動くか
アバターの上を顔認識ボックスが一度だけ走査し、引用カードはVERIFIEDタグとブラケット枠をまといます。

<main class="stage">
<div class="testimonial is-demo">
<div class="testimonial__avatar" aria-hidden="true">
<span class="testimonial__scanbox"></span>
</div>
<figure class="testimonial__card">
<span class="testimonial__corner testimonial__corner--tl" aria-hidden="true"></span>
<span class="testimonial__corner testimonial__corner--tr" aria-hidden="true"></span>
<span class="testimonial__corner testimonial__corner--bl" aria-hidden="true"></span>
<span class="testimonial__corner testimonial__corner--br" aria-hidden="true"></span>
<span class="testimonial__verified">VERIFIED</span>
<blockquote class="testimonial__quote">見逃しがないので安心です。</blockquote>
<figcaption class="testimonial__author">イ・ドユン · セキュリティチームマネージャー</figcaption>
</figure>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 얼굴 인식 상자가 한 번 훑고 멈추는 한 주기
$easing: $ease-spring; // 스캔 상자가 내려가는 곡선
$color: $stage-yellow; // 호박색 터미널 강조색
@include stage($stage-yellow);
.testimonial {
display: flex;
align-items: center;
gap: $sp-4;
width: clamp(260px, 84vw, 420px);
}
.testimonial__avatar {
position: relative;
flex: none;
width: 64px;
height: 64px;
border-radius: $r-control;
background: $subject-ink;
box-shadow: 0 0 0 1px rgba($color, .4);
overflow: hidden;
}
.testimonial__scanbox {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 3px;
background: rgba($color, .85);
box-shadow: 0 0 8px 1px rgba($color, .6);
animation: faceScan $duration $easing infinite;
}
@keyframes faceScan {
0% { transform: translateY(0); opacity: 1; }
20% { transform: translateY(61px); opacity: 1; }
28% { transform: translateY(61px); opacity: 0; }
100% { transform: translateY(61px); opacity: 0; }
}
.testimonial__card {
position: relative;
box-sizing: border-box;
min-width: 0;
// flex-wrap:wrap + flex-basis 220px 는 320px 폰 체크(84vw=268.8px)에서 아바타(64px)+gap+220px 가
// 안 들어가 줄바꿈 위험이 있다 (131-06 실측: scrollHeight 203px vs innerHeight 200px). 129 형제처럼
// wrap 없이 flex:1 로 균등 분배해 항상 한 줄에 나란히 놓는다.
flex: 1 1 0;
margin: 0;
padding: $sp-4;
border-radius: $r-card;
background: $subject-ink;
box-shadow: 0 0 0 1px rgba($color, .4), 0 0 24px rgba($color, .16);
}
.testimonial__corner {
position: absolute;
width: 14px;
height: 14px;
pointer-events: none;
border-color: $color;
border-style: solid;
border-width: 0;
animation: cornerPulse $duration $easing infinite;
}
.testimonial__corner--tl { top: $sp-2; left: $sp-2; border-top-width: 2px; border-left-width: 2px; transform-origin: top left; } // 규칙8: 괄호 꼭짓점 모서리 고정
.testimonial__corner--tr { top: $sp-2; right: $sp-2; border-top-width: 2px; border-right-width: 2px; transform-origin: top right; animation-delay: -.15s; } // 규칙8: 괄호 꼭짓점 모서리 고정
.testimonial__corner--bl { bottom: $sp-2; left: $sp-2; border-bottom-width: 2px; border-left-width: 2px; transform-origin: bottom left; animation-delay: -.3s; } // 규칙8: 괄호 꼭짓점 모서리 고정
.testimonial__corner--br { bottom: $sp-2; right: $sp-2; border-bottom-width: 2px; border-right-width: 2px; transform-origin: bottom right; animation-delay: -.45s; } // 규칙8: 괄호 꼭짓점 모서리 고정
@keyframes cornerPulse {
0%, 100% { transform: scale(.7); opacity: .5; }
50% { transform: scale(1); opacity: 1; }
}
.testimonial__verified {
position: relative;
display: inline-block;
margin: 0 0 $sp-2;
color: $color;
font-family: "Courier New", monospace;
font-weight: 700;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
letter-spacing: .08em;
}
.testimonial__quote {
position: relative;
margin: 0 0 $sp-2;
color: $subject-cream;
font-weight: 600;
font-size: clamp(12px, 2.8vw, 13px);
line-height: 1.4;
font-family: inherit;
}
.testimonial__author {
position: relative;
margin: 0;
color: rgba($subject-cream, .6);
font-weight: 500;
font-size: clamp(10px, 2.4vw, 11px);
line-height: 1.3;
font-family: inherit;
}
@media (prefers-reduced-motion: reduce) {
.testimonial__scanbox, .testimonial__corner {
animation: none !important;
}
.testimonial__scanbox { opacity: 0; }
.testimonial__corner { transform: scale(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: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.testimonial {
display: flex;
align-items: center;
gap: 16px;
width: clamp(260px, 84vw, 420px);
}
.testimonial__avatar {
position: relative;
flex: none;
width: 64px;
height: 64px;
border-radius: 12px;
background: #17141a;
box-shadow: 0 0 0 1px rgba(255, 210, 63, 0.4);
overflow: hidden;
}
.testimonial__scanbox {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 3px;
background: rgba(255, 210, 63, 0.85);
box-shadow: 0 0 8px 1px rgba(255, 210, 63, 0.6);
animation: faceScan 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes faceScan {
0% {
transform: translateY(0);
opacity: 1;
}
20% {
transform: translateY(61px);
opacity: 1;
}
28% {
transform: translateY(61px);
opacity: 0;
}
100% {
transform: translateY(61px);
opacity: 0;
}
}
.testimonial__card {
position: relative;
box-sizing: border-box;
min-width: 0;
flex: 1 1 0;
margin: 0;
padding: 16px;
border-radius: 16px;
background: #17141a;
box-shadow: 0 0 0 1px rgba(255, 210, 63, 0.4), 0 0 24px rgba(255, 210, 63, 0.16);
}
.testimonial__corner {
position: absolute;
width: 14px;
height: 14px;
pointer-events: none;
border-color: #ffd23f;
border-style: solid;
border-width: 0;
animation: cornerPulse 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.testimonial__corner--tl {
top: 8px;
left: 8px;
border-top-width: 2px;
border-left-width: 2px;
transform-origin: top left;
}
.testimonial__corner--tr {
top: 8px;
right: 8px;
border-top-width: 2px;
border-right-width: 2px;
transform-origin: top right;
animation-delay: -0.15s;
}
.testimonial__corner--bl {
bottom: 8px;
left: 8px;
border-bottom-width: 2px;
border-left-width: 2px;
transform-origin: bottom left;
animation-delay: -0.3s;
}
.testimonial__corner--br {
bottom: 8px;
right: 8px;
border-bottom-width: 2px;
border-right-width: 2px;
transform-origin: bottom right;
animation-delay: -0.45s;
}
@keyframes cornerPulse {
0%, 100% {
transform: scale(0.7);
opacity: 0.5;
}
50% {
transform: scale(1);
opacity: 1;
}
}
.testimonial__verified {
position: relative;
display: inline-block;
margin: 0 0 8px;
color: #ffd23f;
font-family: "Courier New", monospace;
font-weight: 700;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
letter-spacing: 0.08em;
}
.testimonial__quote {
position: relative;
margin: 0 0 8px;
color: #fff7e6;
font-weight: 600;
font-size: clamp(12px, 2.8vw, 13px);
line-height: 1.4;
font-family: inherit;
}
.testimonial__author {
position: relative;
margin: 0;
color: rgba(255, 247, 230, 0.6);
font-weight: 500;
font-size: clamp(10px, 2.4vw, 11px);
line-height: 1.3;
font-family: inherit;
}
@media (prefers-reduced-motion: reduce) {
.testimonial__scanbox, .testimonial__corner {
animation: none !important;
}
.testimonial__scanbox {
opacity: 0;
}
.testimonial__corner {
transform: scale(1);
opacity: 1;
}
}import type { CSSProperties } from "react";
import styles from "./TestimonialHud.module.scss";
export type TestimonialHudProps = {
verified?: string;
quote?: string;
author?: string;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function TestimonialHud({
verified = "VERIFIED",
quote = "놓치는 게 없어서 마음이 편해요",
author = "이도윤 · 보안팀 매니저",
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#ffd23f",
}: TestimonialHudProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.testimonial} style={vars}>
<div className={styles.avatar} aria-hidden="true">
<span className={styles.scanbox} />
</div>
<figure className={styles.card}>
<span className={`${styles.corner} ${styles.cornerTl}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerTr}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerBl}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerBr}`} aria-hidden="true" />
<span className={styles.verified}>{verified}</span>
<blockquote className={styles.quote}>{quote}</blockquote>
<figcaption className={styles.author}>{author}</figcaption>
</figure>
</div>
);
}06 · 顔認識風レビュー — 計測 累積面積 1.751% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
コードで見る所
顧客の声やパートナーの引用のように、「これは確認済みの人の発言だ」という信頼感を視覚的に添えたいときに使います。
<div class="flex flex-wrap items-center gap-4">
<div class="relative h-16 w-16 flex-none overflow-hidden rounded-control bg-ink">
<span class="face-scan absolute left-0 top-0 h-[3px] w-full bg-accent/85"></span>
</div>
<div class="relative min-w-[220px] flex-1 rounded-card bg-ink p-4 shadow-glow">
<span class="corner corner-sm corner-tl" aria-hidden="true"></span>
<span class="corner corner-sm corner-tr" aria-hidden="true"></span>
<span class="mb-2 inline-block text-[10px] font-bold tracking-widest text-accent">VERIFIED</span>
<blockquote class="mb-2 text-sm font-semibold leading-relaxed">見逃しがないので安心です。</blockquote>
<p class="text-xs text-cream/60">イ・ドユン · セキュリティチームマネージャー</p>
</div>
</div>
07アラート帯
何が動くか
画面幅いっぱいの帯の上を斜めのスキャンラインがゆっくり流れ、角のブラケットが脈打ち、ボタンは2秒に一度だけ短いグリッチで揺れます。

<main class="stage">
<div class="ctaband is-demo">
<span class="ctaband__corner ctaband__corner--tl" aria-hidden="true"></span>
<span class="ctaband__corner ctaband__corner--tr" aria-hidden="true"></span>
<span class="ctaband__corner ctaband__corner--bl" aria-hidden="true"></span>
<span class="ctaband__corner ctaband__corner--br" aria-hidden="true"></span>
<div class="ctaband__scan" aria-hidden="true"></div>
<p class="ctaband__eyebrow">ALERT</p>
<h2 class="ctaband__title">今すぐ監視網をオンにしましょう</h2>
<button class="ctaband__cta" type="button">無料トライアルを始める</button>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 대각 스캔 드리프트 + 버튼 글리치 한 주기
$easing: $ease-out; // 스캔이 흐르는 곡선
$color: $stage-yellow; // 호박색 터미널 강조색
@include stage($stage-orange);
.ctaband {
position: relative;
box-sizing: border-box;
width: clamp(280px, 90vw, 520px);
padding: $sp-8 $sp-6;
border-radius: $r-card;
background: $subject-ink;
box-shadow: 0 0 0 1px rgba($color, .4), 0 0 24px rgba($color, .16);
overflow: hidden;
}
.ctaband__scan {
position: absolute;
inset: 0;
pointer-events: none;
background: repeating-linear-gradient(45deg, rgba($color, 0) 0 6px, rgba($color, .12) 6px 7px);
background-size: 200% 200%;
background-position: 0% 0%;
animation: diagDrift $duration linear infinite;
}
@keyframes diagDrift {
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; }
}
.ctaband__corner {
position: absolute;
width: 20px;
height: 20px;
pointer-events: none;
border-color: $color;
border-style: solid;
border-width: 0;
animation: cornerPulse $duration $easing infinite;
}
.ctaband__corner--tl { top: $sp-3; left: $sp-3; border-top-width: 2px; border-left-width: 2px; transform-origin: top left; } // 규칙8: 괄호 꼭짓점 모서리 고정
.ctaband__corner--tr { top: $sp-3; right: $sp-3; border-top-width: 2px; border-right-width: 2px; transform-origin: top right; animation-delay: -.15s; } // 규칙8: 괄호 꼭짓점 모서리 고정
.ctaband__corner--bl { bottom: $sp-3; left: $sp-3; border-bottom-width: 2px; border-left-width: 2px; transform-origin: bottom left; animation-delay: -.3s; } // 규칙8: 괄호 꼭짓점 모서리 고정
.ctaband__corner--br { bottom: $sp-3; right: $sp-3; border-bottom-width: 2px; border-right-width: 2px; transform-origin: bottom right; animation-delay: -.45s; } // 규칙8: 괄호 꼭짓점 모서리 고정
@keyframes cornerPulse {
0%, 100% { transform: scale(.55); opacity: .35; }
50% { transform: scale(1); opacity: 1; }
}
.ctaband__eyebrow {
position: relative;
margin: 0 0 $sp-2;
color: $color;
font-family: "Courier New", monospace;
font-weight: 700;
font-size: clamp(10px, 2.4vw, 11px);
line-height: 1;
letter-spacing: .12em;
}
.ctaband__title {
position: relative;
margin: 0 0 $sp-5;
max-width: 22ch;
color: $subject-cream;
font-weight: 700;
font-size: clamp(18px, 4.4vw, 24px);
line-height: 1.3;
font-family: inherit;
}
.ctaband__cta {
position: relative;
box-sizing: border-box;
border: 1px solid $color;
border-radius: $r-control;
background: rgba($color, .14);
color: $color;
padding: $sp-3 $sp-6;
font-weight: 700;
font-size: clamp(12px, 2.8vw, 13px);
line-height: 1;
font-family: inherit;
letter-spacing: .02em;
animation: ctaGlitch $duration linear infinite;
}
@keyframes ctaGlitch {
0%, 59%, 64%, 100% { transform: translateX(0); opacity: 1; }
60% { transform: translateX(-1px); opacity: .85; }
61% { transform: translateX(1px); opacity: 1; }
62% { transform: translateX(-1px); opacity: .9; }
63% { transform: translateX(0); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
.ctaband__scan, .ctaband__corner, .ctaband__cta {
animation: none !important;
}
.ctaband__corner { transform: scale(1); opacity: 1; }
.ctaband__cta { transform: translateX(0); opacity: 1; }
}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);
}
.ctaband {
position: relative;
box-sizing: border-box;
width: clamp(280px, 90vw, 520px);
padding: 32px 24px;
border-radius: 16px;
background: #17141a;
box-shadow: 0 0 0 1px rgba(255, 210, 63, 0.4), 0 0 24px rgba(255, 210, 63, 0.16);
overflow: hidden;
}
.ctaband__scan {
position: absolute;
inset: 0;
pointer-events: none;
background: repeating-linear-gradient(45deg, rgba(255, 210, 63, 0) 0 6px, rgba(255, 210, 63, 0.12) 6px 7px);
background-size: 200% 200%;
background-position: 0% 0%;
animation: diagDrift 2s linear infinite;
}
@keyframes diagDrift {
0% {
background-position: 0% 0%;
}
100% {
background-position: 100% 100%;
}
}
.ctaband__corner {
position: absolute;
width: 20px;
height: 20px;
pointer-events: none;
border-color: #ffd23f;
border-style: solid;
border-width: 0;
animation: cornerPulse 2s ease-out infinite;
}
.ctaband__corner--tl {
top: 12px;
left: 12px;
border-top-width: 2px;
border-left-width: 2px;
transform-origin: top left;
}
.ctaband__corner--tr {
top: 12px;
right: 12px;
border-top-width: 2px;
border-right-width: 2px;
transform-origin: top right;
animation-delay: -0.15s;
}
.ctaband__corner--bl {
bottom: 12px;
left: 12px;
border-bottom-width: 2px;
border-left-width: 2px;
transform-origin: bottom left;
animation-delay: -0.3s;
}
.ctaband__corner--br {
bottom: 12px;
right: 12px;
border-bottom-width: 2px;
border-right-width: 2px;
transform-origin: bottom right;
animation-delay: -0.45s;
}
@keyframes cornerPulse {
0%, 100% {
transform: scale(0.55);
opacity: 0.35;
}
50% {
transform: scale(1);
opacity: 1;
}
}
.ctaband__eyebrow {
position: relative;
margin: 0 0 8px;
color: #ffd23f;
font-family: "Courier New", monospace;
font-weight: 700;
font-size: clamp(10px, 2.4vw, 11px);
line-height: 1;
letter-spacing: 0.12em;
}
.ctaband__title {
position: relative;
margin: 0 0 20px;
max-width: 22ch;
color: #fff7e6;
font-weight: 700;
font-size: clamp(18px, 4.4vw, 24px);
line-height: 1.3;
font-family: inherit;
}
.ctaband__cta {
position: relative;
box-sizing: border-box;
border: 1px solid #ffd23f;
border-radius: 12px;
background: rgba(255, 210, 63, 0.14);
color: #ffd23f;
padding: 12px 24px;
font-weight: 700;
font-size: clamp(12px, 2.8vw, 13px);
line-height: 1;
font-family: inherit;
letter-spacing: 0.02em;
animation: ctaGlitch 2s linear infinite;
}
@keyframes ctaGlitch {
0%, 59%, 64%, 100% {
transform: translateX(0);
opacity: 1;
}
60% {
transform: translateX(-1px);
opacity: 0.85;
}
61% {
transform: translateX(1px);
opacity: 1;
}
62% {
transform: translateX(-1px);
opacity: 0.9;
}
63% {
transform: translateX(0);
opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) {
.ctaband__scan, .ctaband__corner, .ctaband__cta {
animation: none !important;
}
.ctaband__corner {
transform: scale(1);
opacity: 1;
}
.ctaband__cta {
transform: translateX(0);
opacity: 1;
}
}import type { CSSProperties } from "react";
import styles from "./CtaBandHud.module.scss";
export type CtaBandHudProps = {
eyebrow?: string;
title?: string;
cta?: string;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function CtaBandHud({
eyebrow = "ALERT",
title = "지금 바로 감시망을 켜보세요",
cta = "무료 체험 시작",
duration = 2000,
easing = "ease-out",
color = "#ffd23f",
}: CtaBandHudProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.band} style={vars}>
<span className={`${styles.corner} ${styles.cornerTl}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerTr}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerBl}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerBr}`} aria-hidden="true" />
<div className={styles.scan} aria-hidden="true" />
<p className={styles.eyebrow}>{eyebrow}</p>
<h2 className={styles.title}>{title}</h2>
<button type="button" className={styles.cta}>{cta}</button>
</div>
);
}07 · アラート帯 — 計測 累積面積 48.985% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
コードで見る所
登録誘導帯や無料トライアル開始のように、ページの途中でもう一押ししたい場所に向いています。
<div class="relative overflow-hidden rounded-card bg-ink px-8 py-12 shadow-glow">
<span class="corner corner-tl" aria-hidden="true"></span>
<span class="corner corner-tr" aria-hidden="true"></span>
<span class="corner corner-bl" aria-hidden="true"></span>
<span class="corner corner-br" aria-hidden="true"></span>
<div class="scan-diag absolute inset-0 pointer-events-none"></div>
<p class="mb-2 text-[11px] font-bold uppercase tracking-widest text-accent">ALERT</p>
<h2 class="mb-5 max-w-2xl text-2xl font-bold leading-snug sm:text-3xl">今すぐ監視網をオンにしましょう</h2>
<button class="glitch-btn rounded-control border border-accent bg-accent/10 px-6 py-3 text-sm font-bold text-accent">無料トライアルを始める</button>
</div>
08ターミナルフッター
何が動くか
インク色の背景の上でうっすらしたノイズがフレームごとに位置を変えて点滅し、リンクの区画はデータ表示のように整理され、上端の角にブラケットが残ります。

<main class="stage">
<footer class="footerhud is-demo">
<span class="footerhud__corner footerhud__corner--tl" aria-hidden="true"></span>
<span class="footerhud__corner footerhud__corner--tr" aria-hidden="true"></span>
<div class="footerhud__noise" aria-hidden="true"></div>
<div class="footerhud__cols">
<div class="footerhud__col">
<p class="footerhud__head">SYSTEM</p>
<ul>
<li>ダッシュボード</li>
<li>通知設定</li>
</ul>
</div>
<div class="footerhud__col">
<p class="footerhud__head">SUPPORT</p>
<ul>
<li>お問い合わせ</li>
<li>利用ガイド</li>
</ul>
</div>
<div class="footerhud__col">
<p class="footerhud__head">LEGAL</p>
<ul>
<li>利用規約</li>
<li>プライバシーポリシー</li>
</ul>
</div>
</div>
<p class="footerhud__copy">SENTRY © 2026</p>
</footer>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 노이즈 플리커 프레임 스텝의 한 주기
$easing: $ease-out; // 브래킷이 맥동하는 곡선
$color: $stage-yellow; // 호박색 터미널 강조색
@include stage($stage-paper);
.footerhud {
position: relative;
box-sizing: border-box;
width: clamp(260px, 88vw, 460px);
padding: $sp-6 $sp-5 $sp-4;
border-radius: $r-card;
background: $subject-ink;
box-shadow: 0 0 0 1px rgba($color, .4), 0 0 24px rgba($color, .16);
overflow: hidden;
}
.footerhud__noise {
position: absolute;
inset: 0;
pointer-events: none;
opacity: .06;
background: repeating-linear-gradient(45deg, rgba($color, .9) 0 1px, rgba($color, 0) 1px 4px);
background-size: 12px 12px;
animation: noiseFlicker $duration steps(8) infinite;
}
@keyframes noiseFlicker {
0% { background-position: 0 0; }
100% { background-position: 120px 84px; }
}
.footerhud__corner {
position: absolute;
width: 16px;
height: 16px;
pointer-events: none;
border-color: $color;
border-style: solid;
border-width: 0;
animation: cornerPulse $duration $easing infinite;
}
.footerhud__corner--tl { top: $sp-2; left: $sp-2; border-top-width: 2px; border-left-width: 2px; transform-origin: top left; } // 규칙8: 괄호 꼭짓점 모서리 고정
.footerhud__corner--tr { top: $sp-2; right: $sp-2; border-top-width: 2px; border-right-width: 2px; transform-origin: top right; animation-delay: -.15s; } // 규칙8: 괄호 꼭짓점 모서리 고정
@keyframes cornerPulse {
0%, 100% { transform: scale(.55); opacity: .35; }
50% { transform: scale(1); opacity: 1; }
}
.footerhud__cols {
position: relative;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: $sp-3;
margin: 0 0 $sp-4;
}
.footerhud__col {
min-width: 0;
}
.footerhud__head {
margin: 0 0 $sp-2;
color: $color;
font-family: "Courier New", monospace;
font-weight: 700;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
letter-spacing: .1em;
}
.footerhud__col ul {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: $sp-1;
}
.footerhud__col li {
color: rgba($subject-cream, .7);
font-weight: 500;
font-size: clamp(10px, 2.4vw, 11px);
line-height: 1.4;
font-family: inherit;
}
.footerhud__copy {
position: relative;
margin: 0;
padding-top: $sp-3;
border-top: 1px solid rgba($color, .18);
color: rgba($subject-cream, .72); // 잉크 면 위 9.18:1 — .4 는 3.66 으로 미달
font-family: "Courier New", monospace;
font-weight: 400;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
letter-spacing: .04em;
}
@media (prefers-reduced-motion: reduce) {
.footerhud__noise, .footerhud__corner {
animation: none !important;
}
.footerhud__noise { opacity: .03; }
.footerhud__corner { transform: scale(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: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.footerhud {
position: relative;
box-sizing: border-box;
width: clamp(260px, 88vw, 460px);
padding: 24px 20px 16px;
border-radius: 16px;
background: #17141a;
box-shadow: 0 0 0 1px rgba(255, 210, 63, 0.4), 0 0 24px rgba(255, 210, 63, 0.16);
overflow: hidden;
}
.footerhud__noise {
position: absolute;
inset: 0;
pointer-events: none;
opacity: 0.06;
background: repeating-linear-gradient(45deg, rgba(255, 210, 63, 0.9) 0 1px, rgba(255, 210, 63, 0) 1px 4px);
background-size: 12px 12px;
animation: noiseFlicker 2s steps(8) infinite;
}
@keyframes noiseFlicker {
0% {
background-position: 0 0;
}
100% {
background-position: 120px 84px;
}
}
.footerhud__corner {
position: absolute;
width: 16px;
height: 16px;
pointer-events: none;
border-color: #ffd23f;
border-style: solid;
border-width: 0;
animation: cornerPulse 2s ease-out infinite;
}
.footerhud__corner--tl {
top: 8px;
left: 8px;
border-top-width: 2px;
border-left-width: 2px;
transform-origin: top left;
}
.footerhud__corner--tr {
top: 8px;
right: 8px;
border-top-width: 2px;
border-right-width: 2px;
transform-origin: top right;
animation-delay: -0.15s;
}
@keyframes cornerPulse {
0%, 100% {
transform: scale(0.55);
opacity: 0.35;
}
50% {
transform: scale(1);
opacity: 1;
}
}
.footerhud__cols {
position: relative;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin: 0 0 16px;
}
.footerhud__col {
min-width: 0;
}
.footerhud__head {
margin: 0 0 8px;
color: #ffd23f;
font-family: "Courier New", monospace;
font-weight: 700;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
letter-spacing: 0.1em;
}
.footerhud__col ul {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 4px;
}
.footerhud__col li {
color: rgba(255, 247, 230, 0.7);
font-weight: 500;
font-size: clamp(10px, 2.4vw, 11px);
line-height: 1.4;
font-family: inherit;
}
.footerhud__copy {
position: relative;
margin: 0;
padding-top: 12px;
border-top: 1px solid rgba(255, 210, 63, 0.18);
color: rgba(255, 247, 230, 0.72);
font-family: "Courier New", monospace;
font-weight: 400;
font-size: clamp(9px, 2.2vw, 10px);
line-height: 1;
letter-spacing: 0.04em;
}
@media (prefers-reduced-motion: reduce) {
.footerhud__noise, .footerhud__corner {
animation: none !important;
}
.footerhud__noise {
opacity: 0.03;
}
.footerhud__corner {
transform: scale(1);
opacity: 1;
}
}import type { CSSProperties } from "react";
import styles from "./FooterHud.module.scss";
export type FooterHudColumn = { head: string; links: string[] };
export type FooterHudProps = {
columns?: FooterHudColumn[];
copy?: string;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const defaultColumns: FooterHudColumn[] = [
{ head: "SYSTEM", links: ["대시보드", "알림 설정"] },
{ head: "SUPPORT", links: ["문의하기", "이용 가이드"] },
{ head: "LEGAL", links: ["이용약관", "개인정보처리방침"] },
];
export default function FooterHud({
columns = defaultColumns,
copy = "SENTRY © 2026",
duration = 2000,
easing = "ease-out",
color = "#ffd23f",
}: FooterHudProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<footer className={styles.footer} style={vars}>
<span className={`${styles.corner} ${styles.cornerTl}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerTr}`} aria-hidden="true" />
<div className={styles.noise} aria-hidden="true" />
<div className={styles.cols}>
{columns.map((col) => (
<div key={col.head} className={styles.col}>
<p className={styles.head}>{col.head}</p>
<ul>
{col.links.map((link) => <li key={link}>{link}</li>)}
</ul>
</div>
))}
</div>
<p className={styles.copy}>{copy}</p>
</footer>
);
}08 · ターミナルフッター — 計測 累積面積 29.669% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
コードで見る所
マルチカラムフッターやサイトマップ整理のように、リンクが多い下部領域のトーンを揃えたいときに使います。
<footer class="relative overflow-hidden rounded-t-card bg-ink px-6 pt-8 pb-6 shadow-glow">
<span class="corner corner-tl" aria-hidden="true"></span>
<span class="corner corner-tr" aria-hidden="true"></span>
<div class="noise-flicker absolute inset-0 pointer-events-none"></div>
<div class="relative grid grid-cols-1 gap-6 sm:grid-cols-3">
<div class="min-w-0">
<p class="mb-2 text-[10px] font-bold uppercase tracking-widest text-accent">SYSTEM</p>
<ul class="flex flex-col gap-1 text-xs text-cream/70">
<li>ダッシュボード</li>
<li>通知設定</li>
</ul>
</div>
</div>
<p class="relative mt-6 border-t border-accent/20 pt-3 text-[10px] text-cream/40">SENTRY © 2026</p>
</footer>
09ロックオンボタン・スキャン入力欄
何が動くか
ボタンを押すと4つの角のブラケットが内側にスナップしてロックし、入力欄はフォーカスするとスキャンするように広がる琥珀色のグローが下線に現れます。

<main class="stage">
<form class="formhud is-demo">
<button class="formhud__btn" type="button">
<span class="formhud__corner formhud__corner--tl" aria-hidden="true"></span>
<span class="formhud__corner formhud__corner--tr" aria-hidden="true"></span>
<span class="formhud__corner formhud__corner--bl" aria-hidden="true"></span>
<span class="formhud__corner formhud__corner--br" aria-hidden="true"></span>
ロックオン開始
</button>
<label class="formhud__field">
<input class="formhud__input" type="email" placeholder="メールアドレス" readonly>
<span class="formhud__caret" aria-hidden="true"></span>
</label>
</form>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 버튼 스냅 + 입력창 포커스 글로우 한 주기
$easing: $ease-pop; // 스냅이 튕기는 곡선
$color: $stage-yellow; // 호박색 터미널 강조색
@include stage($stage-ink);
.formhud {
display: flex;
flex-direction: column;
gap: $sp-4;
width: clamp(220px, 78vw, 300px);
}
.formhud__btn {
position: relative;
box-sizing: border-box;
border: 1px solid $color;
border-radius: $r-control;
background: rgba($color, .14);
color: $color;
padding: $sp-3 $sp-5;
font-weight: 700;
font-size: clamp(12px, 2.8vw, 13px);
line-height: 1;
font-family: inherit;
letter-spacing: .02em;
animation: btnSnap $duration $easing infinite;
}
@keyframes btnSnap {
0%, 55%, 100% { transform: scale(1); }
75% { transform: scale(.97); }
}
.formhud__corner {
position: absolute;
width: 10px;
height: 10px;
pointer-events: none;
border-color: $color;
border-style: solid;
border-width: 0;
}
.formhud__corner--tl { top: $sp-1; left: $sp-1; border-top-width: 2px; border-left-width: 2px; animation: snapTl $duration $easing infinite; }
.formhud__corner--tr { top: $sp-1; right: $sp-1; border-top-width: 2px; border-right-width: 2px; animation: snapTr $duration $easing infinite; }
.formhud__corner--bl { bottom: $sp-1; left: $sp-1; border-bottom-width: 2px; border-left-width: 2px; animation: snapBl $duration $easing infinite; }
.formhud__corner--br { bottom: $sp-1; right: $sp-1; border-bottom-width: 2px; border-right-width: 2px; animation: snapBr $duration $easing infinite; }
@keyframes snapTl { 0%, 55%, 100% { transform: translate(0, 0); } 75% { transform: translate(3px, 3px); } }
@keyframes snapTr { 0%, 55%, 100% { transform: translate(0, 0); } 75% { transform: translate(-3px, 3px); } }
@keyframes snapBl { 0%, 55%, 100% { transform: translate(0, 0); } 75% { transform: translate(3px, -3px); } }
@keyframes snapBr { 0%, 55%, 100% { transform: translate(0, 0); } 75% { transform: translate(-3px, -3px); } }
.formhud__field {
position: relative;
display: block;
border-radius: $r-sm;
background: $subject-ink;
box-shadow: 0 1px 0 0 rgba($color, .4);
animation: focusGlow $duration $easing infinite;
}
@keyframes focusGlow {
0%, 55%, 100% { box-shadow: 0 1px 0 0 rgba($color, .4); }
75% { box-shadow: 0 1px 0 0 rgba($color, .9), 0 0 0 3px rgba($color, .25), 0 0 20px rgba($color, .35); }
}
.formhud__input {
box-sizing: border-box;
width: 100%;
border: 0;
background: transparent;
padding: $sp-3 $sp-4;
color: $subject-cream;
font-weight: 500;
font-size: clamp(12px, 2.8vw, 13px);
line-height: 1;
font-family: inherit;
outline: none;
}
.formhud__input::placeholder {
color: rgba($subject-cream, .45);
}
.formhud__caret {
position: absolute;
left: $sp-4;
top: 50%;
width: 2px;
height: 14px;
transform: translateY(-50%);
background: $color;
animation: blink $dur-loop steps(1) infinite;
}
@keyframes blink {
0%, 49% { opacity: 1; }
50%, 100% { opacity: .2; }
}
@media (prefers-reduced-motion: reduce) {
.formhud__btn, .formhud__corner, .formhud__field, .formhud__caret {
animation: none !important;
}
.formhud__btn { transform: scale(1); }
.formhud__corner { transform: translate(0, 0); }
.formhud__field { box-shadow: 0 1px 0 0 rgba($color, .4); }
.formhud__caret { 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);
}
.formhud {
display: flex;
flex-direction: column;
gap: 16px;
width: clamp(220px, 78vw, 300px);
}
.formhud__btn {
position: relative;
box-sizing: border-box;
border: 1px solid #ffd23f;
border-radius: 12px;
background: rgba(255, 210, 63, 0.14);
color: #ffd23f;
padding: 12px 20px;
font-weight: 700;
font-size: clamp(12px, 2.8vw, 13px);
line-height: 1;
font-family: inherit;
letter-spacing: 0.02em;
animation: btnSnap 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
@keyframes btnSnap {
0%, 55%, 100% {
transform: scale(1);
}
75% {
transform: scale(0.97);
}
}
.formhud__corner {
position: absolute;
width: 10px;
height: 10px;
pointer-events: none;
border-color: #ffd23f;
border-style: solid;
border-width: 0;
}
.formhud__corner--tl {
top: 4px;
left: 4px;
border-top-width: 2px;
border-left-width: 2px;
animation: snapTl 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
.formhud__corner--tr {
top: 4px;
right: 4px;
border-top-width: 2px;
border-right-width: 2px;
animation: snapTr 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
.formhud__corner--bl {
bottom: 4px;
left: 4px;
border-bottom-width: 2px;
border-left-width: 2px;
animation: snapBl 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
.formhud__corner--br {
bottom: 4px;
right: 4px;
border-bottom-width: 2px;
border-right-width: 2px;
animation: snapBr 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
@keyframes snapTl {
0%, 55%, 100% {
transform: translate(0, 0);
}
75% {
transform: translate(3px, 3px);
}
}
@keyframes snapTr {
0%, 55%, 100% {
transform: translate(0, 0);
}
75% {
transform: translate(-3px, 3px);
}
}
@keyframes snapBl {
0%, 55%, 100% {
transform: translate(0, 0);
}
75% {
transform: translate(3px, -3px);
}
}
@keyframes snapBr {
0%, 55%, 100% {
transform: translate(0, 0);
}
75% {
transform: translate(-3px, -3px);
}
}
.formhud__field {
position: relative;
display: block;
border-radius: 8px;
background: #17141a;
box-shadow: 0 1px 0 0 rgba(255, 210, 63, 0.4);
animation: focusGlow 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
@keyframes focusGlow {
0%, 55%, 100% {
box-shadow: 0 1px 0 0 rgba(255, 210, 63, 0.4);
}
75% {
box-shadow: 0 1px 0 0 rgba(255, 210, 63, 0.9), 0 0 0 3px rgba(255, 210, 63, 0.25), 0 0 20px rgba(255, 210, 63, 0.35);
}
}
.formhud__input {
box-sizing: border-box;
width: 100%;
border: 0;
background: transparent;
padding: 12px 16px;
color: #fff7e6;
font-weight: 500;
font-size: clamp(12px, 2.8vw, 13px);
line-height: 1;
font-family: inherit;
outline: none;
}
.formhud__input::placeholder {
color: rgba(255, 247, 230, 0.45);
}
.formhud__caret {
position: absolute;
left: 16px;
top: 50%;
width: 2px;
height: 14px;
transform: translateY(-50%);
background: #ffd23f;
animation: blink 2s steps(1) infinite;
}
@keyframes blink {
0%, 49% {
opacity: 1;
}
50%, 100% {
opacity: 0.2;
}
}
@media (prefers-reduced-motion: reduce) {
.formhud__btn, .formhud__corner, .formhud__field, .formhud__caret {
animation: none !important;
}
.formhud__btn {
transform: scale(1);
}
.formhud__corner {
transform: translate(0, 0);
}
.formhud__field {
box-shadow: 0 1px 0 0 rgba(255, 210, 63, 0.4);
}
.formhud__caret {
opacity: 1;
}
}import type { CSSProperties } from "react";
import styles from "./ButtonsInputsHud.module.scss";
export type ButtonsInputsHudProps = {
buttonLabel?: string;
placeholder?: string;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function ButtonsInputsHud({
buttonLabel = "락온 시작",
placeholder = "이메일 주소",
duration = 2000,
easing = "cubic-bezier(.2,1.4,.4,1)",
color = "#ffd23f",
}: ButtonsInputsHudProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<form className={styles.form} style={vars}>
<button type="button" className={styles.btn}>
<span className={`${styles.corner} ${styles.cornerTl}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerTr}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerBl}`} aria-hidden="true" />
<span className={`${styles.corner} ${styles.cornerBr}`} aria-hidden="true" />
{buttonLabel}
</button>
<label className={styles.field}>
<input className={styles.input} type="email" placeholder={placeholder} readOnly />
<span className={styles.caret} aria-hidden="true" />
</label>
</form>
);
}09 · ロックオンボタン・スキャン入力欄 — 計測 累積面積 7.12% · 動いたフレーム 11/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
コードで見る所
フォーム送信ボタンやメール入力欄のように、訪問者の最後の1クリックにしっかりした手応えを返したい場所で使います。
<form class="mx-auto flex max-w-md flex-col gap-3 rounded-card bg-ink p-6 shadow-glow sm:flex-row">
<label class="field-glow relative flex-1 rounded-control bg-ink">
<input class="w-full rounded-control bg-transparent px-4 py-3 text-sm text-cream outline-none" type="email" placeholder="メールアドレス">
</label>
<button class="btn-snap relative rounded-control border border-accent bg-accent/10 px-5 py-3 text-sm font-bold text-accent">
<span class="snap-corner snap-tl" aria-hidden="true"></span>
<span class="snap-corner snap-tr" aria-hidden="true"></span>
<span class="snap-corner snap-bl" aria-hidden="true"></span>
<span class="snap-corner snap-br" aria-hidden="true"></span>
登録する
</button>
</form>
tailwind.config カスタム
上の部品9個とワンページは、色・書体・角丸・影の値をすべてこの設定ひとつだけから読み込んでいます。ここの数値を書き換えるだけで、ブラケットの色からカードの角まで画面全体が一度に変わります。
theme: {
extend: {
colors: {
ink: "#17141a",
cream: "#fff7e6",
accent: "#ffd23f",
data: "#2f6df6",
line: "rgba(255,210,63,.12)",
},
fontFamily: {
display: ["'Space Grotesk'", "system-ui", "sans-serif"],
body: ["'Inter'", "system-ui", "sans-serif"],
},
borderRadius: { control: "12px", card: "16px", pill: "999px" },
boxShadow: {
glow: "0 0 0 1px rgba(255,210,63,.4), 0 0 24px rgba(255,210,63,.16)",
"glow-strong": "0 0 0 1px rgba(255,210,63,.6), 0 0 32px rgba(255,210,63,.4)",
},
spacing: { gutter: "16px" },
},
},
働いている色は実質2つだけです。accent(#ffd23f)の琥珀色がブラケット・状態ドット・ボタンをすべて担当し、data(#2f6df6)の青は座標やID番号のように「これは数値だ」と区別したい場所だけの例外として使います。
バイブコーディング プロンプト
この設定を先にプロジェクトへ入れておけば、Claude CodeやCursorに渡す指示は下の一段落で足ります。ルールまで細かく書いた全文はzip内のprompt.mdに入っています。
背景はink(#17141a)ひとつ、強調はaccent(#ffd23f)の琥珀色ひとつ。カード・ヘッダー・
CTAの角にはすべてL字ブラケットを置き、accent色でうっすら脈打たせる。モノスペース
書体は座標・タイムスタンプ・状態テキストだけに使い、本文の書体はそのままにする。
角丸はrounded-card・rounded-pill・rounded-controlの3種類だけを使う。
この指示をそのままpage/index.htmlに適用し、1280px幅のビューポートでページ全体をキャプチャしました。

どこで壊れるか — 落とし穴
ターミナル ui、どこで壊れるか?
いちばん多い落とし穴は、大きな見出しにmax-widthをかけたまま複数の画面幅を確認しないことです。この記事の07番アラート帯も最初は見出しにmax-w-sm(384px)がかかっていて、見出しの文字がわずか数文字だけ次の行に取り残される不自然な折り返しが起きていました。しかも1280pxのデスクトップのように画面が広くても同じ崩れ方をしていて、原因はビューポート幅ではなくmax-widthの値そのものだったため、狭い画面だけ確認しても見つけられません。max-w-2xlに変えてようやく1280・768・375pxの3幅とも自然に収まりました。モノスペースの数値表示はメール本文へ移した途端に桁の幅から崩れますが、その限界はメルマガ デザインのほうで整理しています。パスワードt2nt5x9vのzipには、この折り返しを直したあとのpage/index.htmlがそのまま入っています。
二つ目の落とし穴は06番の顔認識風レビューです。スキャンボックスが下がる距離(61px)は、アバターの高さ(64px)からボックスの太さを引いた値を固定pxで書き込んだものなので、アバターの大きさを変えるとこの数値も手で計算し直す必要があります。三つ目は03番のカードグリッドのように、走査の順番をnth-childの遅延値で決めている項目です。カードの枚数を4枚から増減させるとanimation-delayの並びも手で合わせ直さないと、走査がカードの途中で重なったり順番が飛んだりします。
アクセシビリティ
押さえた点
9個のデモはすべてMDNが定義するprefers-reduced-motionのメディアクエリを漏れなく入れており、動きを減らす設定をオンにした訪問者の画面には、スキャンやグリッチや脈動の代わりに「すでにロックされた」静止状態(ブラケットは等倍サイズ、ドットとカーソルは常に点灯)だけが残ります。ブラケット・スキャンライン・ノイズの層はすべてaria-hidden="true"で装飾であることを明示し、LOCKED・VERIFIED・LIVEといった状態は色だけでなく文字そのもので伝わるので、色覚特性のある訪問者にも状態が読み取れます。
次に見る所
監視カメラ風以外のデザイントレンドが気になる方はデザイントレンドカテゴリーにまとめてあり、各部品がどんな基準で公開されているかは紹介ページに書いています。
FAQ
実在するゲームやSF映画のサイバーパンク uiをそのまま真似たのですか?
いいえ。この記事の部品は特定のゲーム・映画・サービスの画面を複製したものではなく、ブラケットロックオン・スキャンライン・モノスペースの数値表示といった、監視カメラ的な視覚表現の型だけを参考にしています。サービス名のSENTRYも架空の例として作ったものです。
この暗いトーンを明るい(ライトモードの)サイトにも使えますか?
部分的には可能ですが、あまりおすすめしません。HUD・ターミナルらしさは暗い背景の上でブラケットとスキャンラインが浮き上がることで成り立っており、背景を明るくすると同じコントラストを出すためにaccent色をより濃く使う必要が出てきて、警告やエラーのトーンに近く見えてしまいがちです。逆に明るい地を最初から前提にした構成はパステル デザイン LPキットにまとめてあります。
Tailwindなしの素のCSSでも使えますか?
使えます。zipに同梱したtokens.cssがtailwind.config.jsと同じ値を--color-inkや--color-accentのようなCSS変数として書き出したファイルなので、これ1枚を読み込むだけでTailwindがないプロジェクトでもvar(--color-accent)で同じ琥珀色を再現できます。









