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 원작 · zip 에 SCSS·React 포함 · HUD 디자인, 화면이 나를 겨누는 랜딩 부품 9개
이 데모가 속한 글
HUD 디자인, 화면이 나를 겨누는 랜딩 부품 9개HUD 디자인은 게임이나 SF 영화 속 조준경·레이더처럼, 화면 위에 좌표·상태 수치를 얹어 마치 나를 지켜보고 있는 듯한 인상을 주는 인터페이스 스타일입니다. 이 편은 그 감시 카메라 감성을 실제로 돌아가는 랜딩 이 글의 데모 전부 보기


