02Center-stack badge hero
A sheen sweeps once across the news badge, and below it the headline, the button pair, and four trust marks lift and settle in turn.
Best forLaunch announcements and signup landing pages
translateXanimation-delay:focus-visible

<main class="stage">
<section class="hb is-demo" aria-label="Launch announcement first screen">
<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">Update of the week</span>
</p>
<h1 class="hb__title">New features land here first</h1>
<p class="hb__lead">Sign up and try them before the public release.</p>
<div class="hb__actions">
<a class="hb__btn hb__btn--solid" href="#">Request access</a>
<a class="hb__btn hb__btn--line" href="#">See what changed</a>
</div>
<!-- Trust marks — 실제 로고가 아니라 둥근 사각형 안에 일반 아이콘 + 일반명사 라벨 -->
<ul class="hb__trust" aria-label="Trust marks">
<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">Blocks</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">Links</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">Accuracy</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">Growth</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 Center-stack badge hero — Measured area 3.342% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · Hero Section Design: 9 Patterns, No JS Libraries
The post this demo lives in
Hero Section Design: 9 Patterns, No JS LibrariesHero section design is the layout of the first screen a visitor lands on, and these nine patterns build that screen from See all demos in this post


