02센터 스택 배지 히어로
새 소식 배지 위로 빛줄기가 한 번 지나가고, 그 아래 제목과 버튼 쌍, 맨 아래 신뢰 표시 네 개가 차례로 들렸다 앉습니다.
쓰임출시 안내·가입 유도 랜딩 첫 화면
translateXanimation-delay:focus-visible

<main class="stage">
<section class="hb is-demo" aria-label="출시 안내 첫 화면">
<p class="hb__badge">
<span class="hb__sheen" aria-hidden="true"></span>
<svg viewBox="0 0 256 256" width="13" height="13" aria-hidden="true" focusable="false" fill="currentColor"><path d="M194.82,151.43l-55.09,20.3-20.3,55.09a7.92,7.92,0,0,1-14.86,0l-20.3-55.09-55.09-20.3a7.92,7.92,0,0,1,0-14.86l55.09-20.3,20.3-55.09a7.92,7.92,0,0,1,14.86,0l20.3,55.09,55.09,20.3A7.92,7.92,0,0,1,194.82,151.43Z" opacity="0.2"/><path d="M197.58,129.06,146,110l-19-51.62a15.92,15.92,0,0,0-29.88,0L78,110l-51.62,19a15.92,15.92,0,0,0,0,29.88L78,178l19,51.62a15.92,15.92,0,0,0,29.88,0L146,178l51.62-19a15.92,15.92,0,0,0,0-29.88ZM137,164.22a8,8,0,0,0-4.74,4.74L112,223.85,91.78,169A8,8,0,0,0,87,164.22L32.15,144,87,123.78A8,8,0,0,0,91.78,119L112,64.15,132.22,119a8,8,0,0,0,4.74,4.74L191.85,144ZM144,40a8,8,0,0,1,8-8h16V16a8,8,0,0,1,16,0V32h16a8,8,0,0,1,0,16H184V64a8,8,0,0,1-16,0V48H152A8,8,0,0,1,144,40ZM248,88a8,8,0,0,1-8,8h-8v8a8,8,0,0,1-16,0V96h-8a8,8,0,0,1,0-16h8V72a8,8,0,0,1,16,0v8h8A8,8,0,0,1,248,88Z"/></svg>
<span class="hb__badge-t">이번 주 업데이트</span>
</p>
<h1 class="hb__title">새 기능이 먼저 도착합니다</h1>
<p class="hb__lead">가입하면 공개 전에 미리 써 볼 수 있습니다.</p>
<div class="hb__actions">
<a class="hb__btn hb__btn--solid" href="#">지금 신청하기</a>
<a class="hb__btn hb__btn--line" href="#">변경 내역 보기</a>
</div>
<!-- 신뢰 표시 — 실제 로고가 아니라 둥근 사각형 안에 일반 아이콘 + 일반명사 라벨 -->
<ul class="hb__trust" aria-label="신뢰 표시">
<li class="hb__mark hb__mark--1">
<span class="hb__tile">
<svg viewBox="0 0 256 256" width="15" height="15" aria-hidden="true" focusable="false" fill="currentColor"><path d="M128,129.09V232a8,8,0,0,1-3.84-1l-88-48.16a8,8,0,0,1-4.16-7V80.2a8,8,0,0,1,.7-3.27Z" opacity="0.2"/><path d="M223.68,66.15,135.68,18h0a15.88,15.88,0,0,0-15.36,0l-88,48.17a16,16,0,0,0-8.32,14v95.64a16,16,0,0,0,8.32,14l88,48.17a15.88,15.88,0,0,0,15.36,0l88-48.17a16,16,0,0,0,8.32-14V80.18A16,16,0,0,0,223.68,66.15ZM128,32h0l80.34,44L128,120,47.66,76ZM40,90l80,43.78v85.79L40,175.82Zm96,129.57V133.82L216,90v85.78Z"/></svg>
</span>
<span class="hb__ml">구성</span>
</li>
<li class="hb__mark hb__mark--2">
<span class="hb__tile">
<svg viewBox="0 0 256 256" width="15" height="15" aria-hidden="true" focusable="false" fill="currentColor"><path d="M128,112a36,36,0,1,1,36-36A36,36,0,0,1,128,112Zm60,24a36,36,0,1,0,36,36A36,36,0,0,0,188,136ZM68,136a36,36,0,1,0,36,36A36,36,0,0,0,68,136Z" opacity="0.2"/><path d="M172,76a44,44,0,1,0-44,44A44.05,44.05,0,0,0,172,76Zm-44,28a28,28,0,1,1,28-28A28,28,0,0,1,128,104Zm60,24a44,44,0,1,0,44,44A44.05,44.05,0,0,0,188,128Zm0,72a28,28,0,1,1,28-28A28,28,0,0,1,188,200ZM68,128a44,44,0,1,0,44,44A44.05,44.05,0,0,0,68,128Zm0,72a28,28,0,1,1,28-28A28,28,0,0,1,68,200Z"/></svg>
</span>
<span class="hb__ml">연동</span>
</li>
<li class="hb__mark hb__mark--3">
<span class="hb__tile">
<svg viewBox="0 0 256 256" width="15" height="15" aria-hidden="true" focusable="false" fill="currentColor"><path d="M160,128a32,32,0,1,1-32-32A32,32,0,0,1,160,128Z" opacity="0.2"/><path d="M232,120h-8.34A96.14,96.14,0,0,0,136,32.34V24a8,8,0,0,0-16,0v8.34A96.14,96.14,0,0,0,32.34,120H24a8,8,0,0,0,0,16h8.34A96.14,96.14,0,0,0,120,223.66V232a8,8,0,0,0,16,0v-8.34A96.14,96.14,0,0,0,223.66,136H232a8,8,0,0,0,0-16Zm-96,87.6V200a8,8,0,0,0-16,0v7.6A80.15,80.15,0,0,1,48.4,136H56a8,8,0,0,0,0-16H48.4A80.15,80.15,0,0,1,120,48.4V56a8,8,0,0,0,16,0V48.4A80.15,80.15,0,0,1,207.6,120H200a8,8,0,0,0,0,16h7.6A80.15,80.15,0,0,1,136,207.6ZM128,88a40,40,0,1,0,40,40A40,40,0,0,0,128,88Zm0,64a24,24,0,1,1,24-24A24,24,0,0,1,128,152Z"/></svg>
</span>
<span class="hb__ml">정확도</span>
</li>
<li class="hb__mark hb__mark--4">
<span class="hb__tile">
<svg viewBox="0 0 256 256" width="15" height="15" aria-hidden="true" focusable="false" fill="currentColor"><path d="M232,56v64L168,56Z" opacity="0.2"/><path d="M232,48H168a8,8,0,0,0-5.66,13.66L188.69,88,136,140.69l-34.34-34.35a8,8,0,0,0-11.32,0l-72,72a8,8,0,0,0,11.32,11.32L96,123.31l34.34,34.35a8,8,0,0,0,11.32,0L200,99.31l26.34,26.35A8,8,0,0,0,240,120V56A8,8,0,0,0,232,48Zm-8,52.69L187.31,64H224Z"/></svg>
</span>
<span class="hb__ml">성장</span>
</li>
</ul>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// 275 히어로 팔레트 — 아홉 항목이 같은 값을 쓴다. 괄호 안은 실측 대비비
$hr-ink: $stage-ink; // 옐로 위 글자 12.63:1 · 알약과 타일의 면
$hr-paper: $stage-paper; // 잉크 면 위 글자·아이콘 18.24:1
$hr-yellow: $stage-yellow; // 무대
$hr-violet: #9791c9; // 포커스 링 (옐로 위 3.28:1 — 글자로는 쓰지 않는다)
$duration: $dur-loop; // 빛줄기가 한 번 지나가고 신뢰 표시 넷이 차례로 들리는 한 바퀴
$easing: $ease-out; // 빛줄기의 등속에 가까운 흐름
$color: $hr-ink; // 옐로 무대 위 글자는 전부 잉크 (흰 글자는 1.44:1 이라 금지)
@include stage($hr-yellow);
// 센터 스택 — 배지 · 제목 · 설명 · 버튼 쌍 · 신뢰 표시가 한 축에 쌓인다
.hb {
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-3;
width: clamp(288px, 94vw, 432px);
text-align: center;
}
// ── 새 소식 배지 — 빛줄기가 지나갈 수 있게 overflow 를 닫는다 ──
.hb__badge {
position: relative;
overflow: hidden;
display: inline-flex;
align-items: center;
gap: $sp-1;
margin: 0;
padding: $sp-1 $sp-3;
border-radius: $r-pill;
background: $color;
color: $hr-paper;
font-weight: 700;
font-size: clamp(9px, 2vw, 11px);
line-height: 1.3;
}
.hb__sheen {
position: absolute;
left: 0;
top: 0;
width: 40%;
height: 100%;
background: linear-gradient(100deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, .62) 50%, rgba(255, 255, 255, 0) 100%);
transform: translateX(-160%);
}
.hb__title {
margin: 0;
color: $color;
font-weight: 800;
font-size: clamp(16px, 4.4vw, 27px);
line-height: 1.16;
letter-spacing: -.02em;
}
.hb__lead {
margin: 0;
color: $color;
font-weight: 500;
font-size: clamp(10px, 2.4vw, 13px);
line-height: 1.35;
opacity: .78;
}
.hb__actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: $sp-2;
}
.hb__btn {
display: inline-flex;
align-items: center;
padding: $sp-2 $sp-3;
border: 2px solid $color;
border-radius: $r-control;
font-weight: 700;
font-size: clamp(10px, 2.2vw, 12px);
line-height: 1;
text-decoration: none;
white-space: nowrap;
}
.hb__btn--solid { background: $color; color: $hr-paper; }
.hb__btn--line { color: $color; }
// 터치·키보드에서도 같은 표시
.hb__btn:focus-visible {
outline: 2px solid $hr-violet;
outline-offset: 4px;
}
// ── 신뢰 표시 넷 — 실제 로고가 아니라 일반 도형 + 일반명사 라벨 ──
.hb__trust {
display: flex;
justify-content: center;
gap: $sp-4;
margin: 0;
padding: 0;
list-style: none;
}
.hb__mark {
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-1;
}
.hb__tile {
display: grid;
place-items: center;
width: 30px;
height: 30px;
border-radius: $r-control;
background: $color;
color: $hr-paper;
}
.hb__ml {
color: $color;
font-weight: 700;
font-size: clamp(8px, 1.8vw, 10px);
line-height: 1.2;
}
// ── 자동 재생 ─────────────────────────────────────────────
// animation 단축은 delay 를 0 으로 리셋하므로 낱개 속성으로 쓴다.
// 지연이 걸린 타일에는 animation-fill-mode: backwards 가 반드시 필요하다
.hb.is-demo .hb__sheen {
animation-name: hbSheen;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.hb.is-demo .hb__tile {
animation-name: hbLift;
animation-duration: $duration;
animation-timing-function: $ease-spring;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.hb.is-demo .hb__mark--2 .hb__tile { animation-delay: .14s; }
.hb.is-demo .hb__mark--3 .hb__tile { animation-delay: .28s; }
.hb.is-demo .hb__mark--4 .hb__tile { animation-delay: .42s; }
// 빛줄기는 알약 밖(오른쪽)에서 멈춰 있다가 다음 바퀴에 왼쪽으로 되돌아간다 —
// overflow: hidden 이 닫고 있으므로 되감기가 화면에 보이지 않는다
@keyframes hbSheen {
0% { transform: translateX(-160%); }
70%, 100% { transform: translateX(320%); }
}
// 들렸다 앉기 — 드는 쪽이 빠르고 앉는 쪽이 느려야 대표 이미지가 들린 장면이 된다.
// 복귀는 88% 안에 끝낸다 (24프레임의 마지막 장이 95.8% 지점)
@keyframes hbLift {
0% { transform: translateY(0); }
16%, 46% { transform: translateY(-7px); }
88%, 100% { transform: translateY(0); }
}
// 좁은 화면 — 한 축을 유지한 채 글자·타일만 줄인다. 여기서는 간격을 올리지 않는다
@media (max-width: 340px), (max-height: 220px) {
.hb { gap: $sp-1; width: 100%; }
.hb__badge { padding: 0 $sp-2; }
.hb__badge svg { width: 11px; height: 11px; }
.hb__btn { padding: $sp-1 $sp-2; }
.hb__trust { gap: $sp-2; }
.hb__tile { width: 24px; height: 24px; border-radius: $r-sm; }
.hb__tile svg { width: 13px; height: 13px; }
}
@media (prefers-reduced-motion: reduce) {
// .hb.is-demo .hb__tile (0,3,0) 를 이기려면 !important 가 필요하다.
// 움직임만 끄고 배지·타일·라벨은 그대로 남는다
.hb__sheen, .hb__tile { animation: none !important; }
.hb__sheen { opacity: 0 !important; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.hb {
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
width: clamp(288px, 94vw, 432px);
text-align: center;
}
.hb__badge {
position: relative;
overflow: hidden;
display: inline-flex;
align-items: center;
gap: 4px;
margin: 0;
padding: 4px 12px;
border-radius: 999px;
background: #17141a;
color: #ffffff;
font-weight: 700;
font-size: clamp(9px, 2vw, 11px);
line-height: 1.3;
}
.hb__sheen {
position: absolute;
left: 0;
top: 0;
width: 40%;
height: 100%;
background: linear-gradient(100deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.62) 50%, rgba(255, 255, 255, 0) 100%);
transform: translateX(-160%);
}
.hb__title {
margin: 0;
color: #17141a;
font-weight: 800;
font-size: clamp(16px, 4.4vw, 27px);
line-height: 1.16;
letter-spacing: -0.02em;
}
.hb__lead {
margin: 0;
color: #17141a;
font-weight: 500;
font-size: clamp(10px, 2.4vw, 13px);
line-height: 1.35;
opacity: 0.78;
}
.hb__actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 8px;
}
.hb__btn {
display: inline-flex;
align-items: center;
padding: 8px 12px;
border: 2px solid #17141a;
border-radius: 12px;
font-weight: 700;
font-size: clamp(10px, 2.2vw, 12px);
line-height: 1;
text-decoration: none;
white-space: nowrap;
}
.hb__btn--solid {
background: #17141a;
color: #ffffff;
}
.hb__btn--line {
color: #17141a;
}
.hb__btn:focus-visible {
outline: 2px solid #9791c9;
outline-offset: 4px;
}
.hb__trust {
display: flex;
justify-content: center;
gap: 16px;
margin: 0;
padding: 0;
list-style: none;
}
.hb__mark {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.hb__tile {
display: grid;
place-items: center;
width: 30px;
height: 30px;
border-radius: 12px;
background: #17141a;
color: #ffffff;
}
.hb__ml {
color: #17141a;
font-weight: 700;
font-size: clamp(8px, 1.8vw, 10px);
line-height: 1.2;
}
.hb.is-demo .hb__sheen {
animation-name: hbSheen;
animation-duration: 2s;
animation-timing-function: ease-out;
animation-iteration-count: infinite;
}
.hb.is-demo .hb__tile {
animation-name: hbLift;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.hb.is-demo .hb__mark--2 .hb__tile {
animation-delay: 0.14s;
}
.hb.is-demo .hb__mark--3 .hb__tile {
animation-delay: 0.28s;
}
.hb.is-demo .hb__mark--4 .hb__tile {
animation-delay: 0.42s;
}
@keyframes hbSheen {
0% {
transform: translateX(-160%);
}
70%, 100% {
transform: translateX(320%);
}
}
@keyframes hbLift {
0% {
transform: translateY(0);
}
16%, 46% {
transform: translateY(-7px);
}
88%, 100% {
transform: translateY(0);
}
}
@media (max-width: 340px), (max-height: 220px) {
.hb {
gap: 4px;
width: 100%;
}
.hb__badge {
padding: 0 8px;
}
.hb__badge svg {
width: 11px;
height: 11px;
}
.hb__btn {
padding: 4px 8px;
}
.hb__trust {
gap: 8px;
}
.hb__tile {
width: 24px;
height: 24px;
border-radius: 8px;
}
.hb__tile svg {
width: 13px;
height: 13px;
}
}
@media (prefers-reduced-motion: reduce) {
.hb__sheen, .hb__tile {
animation: none !important;
}
.hb__sheen {
opacity: 0 !important;
}
}(function () {
var root = document.querySelector('.hb');
var btns = [].slice.call(root.querySelectorAll('.hb__btn'));
function stopLoop() { root.classList.remove('is-demo'); }
// 버튼 쌍은 좌우 화살표로 오간다 (Home·End 는 양 끝).
// preventScroll 이 없으면 iframe 안 포커스가 부모 홈 페이지를 아래로 끌어당긴다
btns.forEach(function (btn, i) {
btn.addEventListener('keydown', function (e) {
var to = -1;
if (e.key === 'ArrowRight') { to = (i + 1) % btns.length; }
if (e.key === 'ArrowLeft') { to = (i - 1 + btns.length) % btns.length; }
if (e.key === 'Home') { to = 0; }
if (e.key === 'End') { to = btns.length - 1; }
if (to < 0) { return; }
e.preventDefault();
stopLoop();
btns[to].focus({ preventScroll: true });
});
});
root.addEventListener('pointerenter', stopLoop);
root.addEventListener('focusin', stopLoop);
})();import { useRef, useState } from "react";
import type { CSSProperties, ReactNode } from "react";
import styles from "./HeroCenterBadge.module.scss";
/* ambient 에 KeyboardEvent 가 없어 파일 안에 필요한 만큼만 선언한다 */
type KeyEvt = { key: string; preventDefault(): void };
type Box<T> = { current: T };
export type TrustMark = {
/** 일반명사 라벨 — 실제 브랜드 이름을 쓰지 않는다 */
label: string;
icon: ReactNode;
};
export type HeroCenterBadgeProps = {
badge?: string;
title?: string;
lead?: string;
solid?: string;
line?: string;
trustLabel?: string;
marks?: TrustMark[];
sectionLabel?: string;
/** 빛줄기가 한 번 지나가고 신뢰 표시 넷이 차례로 들리는 한 바퀴(ms) */
duration?: number;
easing?: string;
/** 옐로 무대 위 글자·면의 색 — 잉크 고정을 권한다 (흰 글자는 1.44:1) */
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const CubeIcon = (): ReactNode => (
<svg viewBox="0 0 256 256" width="15" height="15" aria-hidden="true" focusable="false" fill="currentColor">
<path d="M128,129.09V232a8,8,0,0,1-3.84-1l-88-48.16a8,8,0,0,1-4.16-7V80.2a8,8,0,0,1,.7-3.27Z" opacity="0.2" />
<path d="M223.68,66.15,135.68,18h0a15.88,15.88,0,0,0-15.36,0l-88,48.17a16,16,0,0,0-8.32,14v95.64a16,16,0,0,0,8.32,14l88,48.17a15.88,15.88,0,0,0,15.36,0l88-48.17a16,16,0,0,0,8.32-14V80.18A16,16,0,0,0,223.68,66.15ZM128,32h0l80.34,44L128,120,47.66,76ZM40,90l80,43.78v85.79L40,175.82Zm96,129.57V133.82L216,90v85.78Z" />
</svg>
);
const CirclesIcon = (): ReactNode => (
<svg viewBox="0 0 256 256" width="15" height="15" aria-hidden="true" focusable="false" fill="currentColor">
<path d="M128,112a36,36,0,1,1,36-36A36,36,0,0,1,128,112Zm60,24a36,36,0,1,0,36,36A36,36,0,0,0,188,136ZM68,136a36,36,0,1,0,36,36A36,36,0,0,0,68,136Z" opacity="0.2" />
<path d="M172,76a44,44,0,1,0-44,44A44.05,44.05,0,0,0,172,76Zm-44,28a28,28,0,1,1,28-28A28,28,0,0,1,128,104Zm60,24a44,44,0,1,0,44,44A44.05,44.05,0,0,0,188,128Zm0,72a28,28,0,1,1,28-28A28,28,0,0,1,188,200ZM68,128a44,44,0,1,0,44,44A44.05,44.05,0,0,0,68,128Zm0,72a28,28,0,1,1,28-28A28,28,0,0,1,68,200Z" />
</svg>
);
const CrosshairIcon = (): ReactNode => (
<svg viewBox="0 0 256 256" width="15" height="15" aria-hidden="true" focusable="false" fill="currentColor">
<path d="M160,128a32,32,0,1,1-32-32A32,32,0,0,1,160,128Z" opacity="0.2" />
<path d="M232,120h-8.34A96.14,96.14,0,0,0,136,32.34V24a8,8,0,0,0-16,0v8.34A96.14,96.14,0,0,0,32.34,120H24a8,8,0,0,0,0,16h8.34A96.14,96.14,0,0,0,120,223.66V232a8,8,0,0,0,16,0v-8.34A96.14,96.14,0,0,0,223.66,136H232a8,8,0,0,0,0-16Zm-96,87.6V200a8,8,0,0,0-16,0v7.6A80.15,80.15,0,0,1,48.4,136H56a8,8,0,0,0,0-16H48.4A80.15,80.15,0,0,1,120,48.4V56a8,8,0,0,0,16,0V48.4A80.15,80.15,0,0,1,207.6,120H200a8,8,0,0,0,0,16h7.6A80.15,80.15,0,0,1,136,207.6ZM128,88a40,40,0,1,0,40,40A40,40,0,0,0,128,88Zm0,64a24,24,0,1,1,24-24A24,24,0,0,1,128,152Z" />
</svg>
);
const TrendUpIcon = (): ReactNode => (
<svg viewBox="0 0 256 256" width="15" height="15" aria-hidden="true" focusable="false" fill="currentColor">
<path d="M232,56v64L168,56Z" opacity="0.2" />
<path d="M232,48H168a8,8,0,0,0-5.66,13.66L188.69,88,136,140.69l-34.34-34.35a8,8,0,0,0-11.32,0l-72,72a8,8,0,0,0,11.32,11.32L96,123.31l34.34,34.35a8,8,0,0,0,11.32,0L200,99.31l26.34,26.35A8,8,0,0,0,240,120V56A8,8,0,0,0,232,48Zm-8,52.69L187.31,64H224Z" />
</svg>
);
const DEFAULT_MARKS: TrustMark[] = [
{ label: "구성", icon: <CubeIcon /> },
{ label: "연동", icon: <CirclesIcon /> },
{ label: "정확도", icon: <CrosshairIcon /> },
{ label: "성장", icon: <TrendUpIcon /> },
];
export default function HeroCenterBadge({
badge = "이번 주 업데이트",
title = "새 기능이 먼저 도착합니다",
lead = "가입하면 공개 전에 미리 써 볼 수 있습니다.",
solid = "지금 신청하기",
line = "변경 내역 보기",
trustLabel = "신뢰 표시",
marks = DEFAULT_MARKS,
sectionLabel = "출시 안내 첫 화면",
duration = 2000,
easing = "ease-out",
color = "#17141a",
}: HeroCenterBadgeProps): ReactNode {
const [loop, setLoop] = useState<boolean>(true);
const refs = useRef<Array<HTMLAnchorElement | null>>([]) as Box<Array<HTMLAnchorElement | null>>;
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
const tone = [styles.mark1, styles.mark2, styles.mark3, styles.mark4];
/* 버튼 쌍은 좌우 화살표로 오간다. preventScroll 이 없으면 iframe 안 포커스가
부모 페이지를 아래로 끌어당긴다 */
const onKey = (e: KeyEvt, i: number): void => {
const list = refs.current;
if (!list) { return; }
let to = -1;
if (e.key === "ArrowRight") { to = (i + 1) % list.length; }
if (e.key === "ArrowLeft") { to = (i - 1 + list.length) % list.length; }
if (e.key === "Home") { to = 0; }
if (e.key === "End") { to = list.length - 1; }
if (to < 0) { return; }
e.preventDefault();
setLoop(false);
const next = list[to];
if (next) { next.focus({ preventScroll: true }); }
};
const labels = [solid, line];
return (
<section
className={loop ? `${styles.hb} ${styles.isDemo}` : styles.hb}
style={vars}
aria-label={sectionLabel}
onPointerEnter={() => setLoop(false)}
onFocus={() => setLoop(false)}
>
<p className={styles.badge}>
<span className={styles.sheen} aria-hidden="true" />
<svg viewBox="0 0 256 256" width="13" height="13" aria-hidden="true" focusable="false" fill="currentColor">
<path d="M194.82,151.43l-55.09,20.3-20.3,55.09a7.92,7.92,0,0,1-14.86,0l-20.3-55.09-55.09-20.3a7.92,7.92,0,0,1,0-14.86l55.09-20.3,20.3-55.09a7.92,7.92,0,0,1,14.86,0l20.3,55.09,55.09,20.3A7.92,7.92,0,0,1,194.82,151.43Z" opacity="0.2" />
<path d="M197.58,129.06,146,110l-19-51.62a15.92,15.92,0,0,0-29.88,0L78,110l-51.62,19a15.92,15.92,0,0,0,0,29.88L78,178l19,51.62a15.92,15.92,0,0,0,29.88,0L146,178l51.62-19a15.92,15.92,0,0,0,0-29.88ZM137,164.22a8,8,0,0,0-4.74,4.74L112,223.85,91.78,169A8,8,0,0,0,87,164.22L32.15,144,87,123.78A8,8,0,0,0,91.78,119L112,64.15,132.22,119a8,8,0,0,0,4.74,4.74L191.85,144ZM144,40a8,8,0,0,1,8-8h16V16a8,8,0,0,1,16,0V32h16a8,8,0,0,1,0,16H184V64a8,8,0,0,1-16,0V48H152A8,8,0,0,1,144,40ZM248,88a8,8,0,0,1-8,8h-8v8a8,8,0,0,1-16,0V96h-8a8,8,0,0,1,0-16h8V72a8,8,0,0,1,16,0v8h8A8,8,0,0,1,248,88Z" />
</svg>
<span className={styles.badgeT}>{badge}</span>
</p>
<h1 className={styles.title}>{title}</h1>
<p className={styles.lead}>{lead}</p>
<div className={styles.actions}>
{labels.map((t: string, i: number) => (
<a
className={`${styles.btn} ${i === 0 ? styles.btnSolid : styles.btnLine}`}
href="#"
key={t}
ref={(el: HTMLAnchorElement | null) => { if (refs.current) { refs.current[i] = el; } }}
onKeyDown={(e: KeyEvt) => onKey(e, i)}
>
{t}
</a>
))}
</div>
{/* 신뢰 표시 — 실제 로고가 아니라 둥근 사각형 안에 일반 아이콘 + 일반명사 라벨 */}
<ul className={styles.trust} aria-label={trustLabel}>
{marks.map((m: TrustMark, i: number) => (
<li className={`${styles.mark} ${tone[i % tone.length]}`} key={m.label}>
<span className={styles.tile}>{m.icon}</span>
<span className={styles.ml}>{m.label}</span>
</li>
))}
</ul>
</section>
);
}02 센터 스택 배지 히어로 — 측정 누적면적 3.342% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 히어로 섹션 디자인 패턴 9종 — JS 라이브러리 없이
이 데모가 속한 글
히어로 섹션 디자인 패턴 9종 — JS 라이브러리 없이히어로 섹션 디자인은 사이트를 열자마자 나타나는 첫 화면의 구도를 짜는 일입니다. 여기 아홉 가지는 스플릿·센터 스택·글래스 카드·슬라이더 같은 배치 패턴을 HTML 과 CSS 만으로 짜 둔 것이라, JS 라이브러리 이 글의 데모 전부 보기


