09Minimal logo hero
Only a centered logo mark, one line of copy, and a single button remain, while a ring quietly spreads out from the mark and fades.
Best forBrand teasers and coming-soon pages
scaleopacity:focus-visible

<main class="stage">
<section class="hm is-demo" aria-label="Coming soon notice">
<div class="hm__mark">
<!-- 고리 두 겹이 시차를 두고 퍼진다. 주인공(로고 표시)은 루프 내내 제자리에 남는다 -->
<span class="hm__ring hm__ring--a" aria-hidden="true"></span>
<span class="hm__ring hm__ring--b" aria-hidden="true"></span>
<span class="hm__badge">
<svg viewBox="0 0 256 256" width="30" height="30" 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>
</div>
<p class="hm__copy">Opening soon</p>
<button class="hm__btn" type="button" aria-pressed="false">Tell me when it opens</button>
<p class="hm__live" role="status" aria-live="polite">Kept on this screen only, no email</p>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// 275 히어로 팔레트 — 아홉 항목이 같은 값을 쓴다. 괄호 안은 실측 대비비
$hr-ink: $stage-ink; // 무대
$hr-paper: $stage-paper; // 배지 아이콘 on 바이올렛 6.14:1
$hr-mist: #b6b0c0; // 잉크 위 보조 글자 8.65:1
$hr-yellow: $stage-yellow; // 버튼 면 (잉크 글자 12.63:1)
$hr-violet: #9791c9; // 로고 배지 면
$hr-lilac: #d8d6eb; // 잉크 위 강조 글자·고리 6.99:1
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-loop; // 고리 한 겹이 퍼졌다 사라지는 한 바퀴
$easing: $ease-out; // 퍼질수록 느려진다
$color: $hr-lilac; // 고리 색
@include stage($hr-ink);
.hm {
display: grid;
justify-items: center;
gap: $sp-3;
width: min(360px, 90vw);
text-align: center;
}
.hm__mark {
position: relative;
display: grid;
place-items: center;
width: 72px;
height: 72px;
}
// 정적 상태 — 루프가 없어도 고리 두 겹이 그대로 보인다(.is-demo 를 뗀 화면이 비면 안 된다)
.hm__ring {
position: absolute;
inset: 0;
border: 3px solid $color; // 2px 는 480×300 축소본(격자 카드)에서 고리가 거의 안 보였다
border-radius: 50%;
scale: 1;
opacity: .34;
}
.hm__ring--b { scale: 1.34; opacity: .18; }
.hm__badge {
display: grid;
place-items: center;
width: 56px;
height: 56px;
border-radius: $r-card;
background: $hr-violet;
color: $hr-paper;
}
.hm__copy {
margin: 0;
color: $hr-mist;
font-weight: 600;
font-size: clamp(13px, 3.6vw, 15px);
line-height: 1.3;
}
.hm__btn {
margin: 0;
padding: $sp-2 $sp-5;
border: 0;
border-radius: $r-pill;
background: $hr-yellow;
color: $hr-ink;
font-family: inherit;
font-weight: 700;
font-size: 13px;
line-height: 1.2;
cursor: pointer;
transition: transform $dur-quick $ease-spring;
}
.hm__btn:hover { transform: translateY(-2px); }
.hm__btn:focus-visible { outline: 2px solid $color; outline-offset: 3px; }
.hm__live {
margin: 0;
color: $color;
font-weight: 500;
font-size: 11px;
line-height: 1.3;
}
// ── 격자 미리보기 루프 ────────────────────────────────
// 고리는 scale 로 퍼지며 opacity 로 사라진다. 두 겹이 한 바퀴의 절반씩 어긋난다.
// animation 단축은 delay 를 0 으로 리셋하므로 낱개 속성으로만 쓴다
.hm.is-demo .hm__ring {
animation-name: hmRing;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.hm.is-demo .hm__ring--b { animation-delay: -1s; }
// 한 바퀴를 88% 안에 끝낸다 — 24프레임의 마지막 장이 95.8% 지점이라 그 뒤는 찍히지 않는다.
// 되돌아오는 구간(88%→100%)은 opacity 0 이라 눈에 안 띈다
@keyframes hmRing {
0% { scale: .58; opacity: .62; }
88% { scale: 1.9; opacity: 0; }
88.01%, 100% { scale: .58; opacity: 0; }
}
// 좁은 화면 — 글자와 여백만 줄인다. 값을 올리지 않는다.
// 고리는 scale 로 커진 만큼 경계 상자도 커져 scrollHeight 에 잡힌다(219 실측) — 표시 자체를 줄인다
@media (max-width: 340px), (max-height: 220px) {
.hm { gap: $sp-2; }
.hm__mark { width: 56px; height: 56px; }
.hm__badge { width: 44px; height: 44px; }
.hm__badge svg { width: 24px; height: 24px; }
.hm__btn { padding: $sp-2 $sp-4; font-size: 12px; }
.hm__live { font-size: 10px; }
}
@media (prefers-reduced-motion: reduce) {
// .hm.is-demo .hm__ring (0,3,0) 를 이기려면 !important 가 필요하다.
// 움직임만 끄고 고리·글자·버튼은 그대로 남긴다
.hm__ring { animation: none !important; }
.hm__ring--a { scale: 1 !important; opacity: .34 !important; }
.hm__ring--b { scale: 1.34 !important; opacity: .18 !important; }
.hm__btn { transition: none !important; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.hm {
display: grid;
justify-items: center;
gap: 12px;
width: min(360px, 90vw);
text-align: center;
}
.hm__mark {
position: relative;
display: grid;
place-items: center;
width: 72px;
height: 72px;
}
.hm__ring {
position: absolute;
inset: 0;
border: 3px solid #d8d6eb;
border-radius: 50%;
scale: 1;
opacity: 0.34;
}
.hm__ring--b {
scale: 1.34;
opacity: 0.18;
}
.hm__badge {
display: grid;
place-items: center;
width: 56px;
height: 56px;
border-radius: 16px;
background: #9791c9;
color: #ffffff;
}
.hm__copy {
margin: 0;
color: #b6b0c0;
font-weight: 600;
font-size: clamp(13px, 3.6vw, 15px);
line-height: 1.3;
}
.hm__btn {
margin: 0;
padding: 8px 20px;
border: 0;
border-radius: 999px;
background: #ffd23f;
color: #17141a;
font-family: inherit;
font-weight: 700;
font-size: 13px;
line-height: 1.2;
cursor: pointer;
transition: transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.hm__btn:hover {
transform: translateY(-2px);
}
.hm__btn:focus-visible {
outline: 2px solid #d8d6eb;
outline-offset: 3px;
}
.hm__live {
margin: 0;
color: #d8d6eb;
font-weight: 500;
font-size: 11px;
line-height: 1.3;
}
.hm.is-demo .hm__ring {
animation-name: hmRing;
animation-duration: 2s;
animation-timing-function: ease-out;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.hm.is-demo .hm__ring--b {
animation-delay: -1s;
}
@keyframes hmRing {
0% {
scale: 0.58;
opacity: 0.62;
}
88% {
scale: 1.9;
opacity: 0;
}
88.01%, 100% {
scale: 0.58;
opacity: 0;
}
}
@media (max-width: 340px), (max-height: 220px) {
.hm {
gap: 8px;
}
.hm__mark {
width: 56px;
height: 56px;
}
.hm__badge {
width: 44px;
height: 44px;
}
.hm__badge svg {
width: 24px;
height: 24px;
}
.hm__btn {
padding: 8px 16px;
font-size: 12px;
}
.hm__live {
font-size: 10px;
}
}
@media (prefers-reduced-motion: reduce) {
.hm__ring {
animation: none !important;
}
.hm__ring--a {
scale: 1 !important;
opacity: 0.34 !important;
}
.hm__ring--b {
scale: 1.34 !important;
opacity: 0.18 !important;
}
.hm__btn {
transition: none !important;
}
}(function () {
var root = document.querySelector('.hm');
var btn = root.querySelector('.hm__btn');
var live = root.querySelector('.hm__live');
var waiting = false;
// 버튼을 누르면 aria-live 한 줄이 바뀐다 (실제 등록이 아니라 이 화면 안의 표시)
btn.addEventListener('click', function () {
waiting = !waiting;
live.textContent = waiting ? 'Marked as waiting for the notice' : 'Kept on this screen only, no email';
btn.textContent = waiting ? 'Cancel the notice' : 'Tell me when it opens';
btn.setAttribute('aria-pressed', waiting ? 'true' : 'false');
});
// 사람이 손을 대면 미리보기 루프를 뗀다
function stopLoop() { root.classList.remove('is-demo'); }
root.addEventListener('pointerdown', stopLoop);
root.addEventListener('focusin', stopLoop);
})();import { useState } from "react";
import type { CSSProperties, ReactNode } from "react";
import styles from "./HeroMinimalLogoPulse.module.scss";
export type HeroMinimalLogoPulseProps = {
label?: string;
copy?: string;
cta?: string;
ctaOn?: string;
note?: string;
noteOn?: string;
/** 고리 한 겹이 퍼졌다 사라지는 한 바퀴(ms) */
duration?: number;
easing?: string;
/** 고리·안내 줄의 강조색 */
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const CirclesThreeIcon = (): ReactNode => (
<svg viewBox="0 0 256 256" width="30" height="30" 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>
);
export default function HeroMinimalLogoPulse({
label = "준비 중 안내",
copy = "곧 문을 엽니다",
cta = "열리면 알려 주세요",
ctaOn = "알림 받기 취소",
note = "이메일 없이 이 화면에서만 받습니다",
noteOn = "알림 대기자로 표시했습니다",
duration = 2000,
easing = "ease-out",
color = "#9d93ff",
}: HeroMinimalLogoPulseProps): ReactNode {
const [loop, setLoop] = useState<boolean>(true);
const [on, setOn] = useState<boolean>(false);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section
className={loop ? `${styles.hm} ${styles.isDemo}` : styles.hm}
style={vars}
aria-label={label}
onPointerDown={() => setLoop(false)}
onFocus={() => setLoop(false)}
>
<div className={styles.mark}>
{/* 고리 두 겹이 시차를 두고 퍼진다. 주인공(로고 표시)은 루프 내내 제자리에 남는다 */}
<span className={`${styles.ring} ${styles.ringA}`} aria-hidden="true" />
<span className={`${styles.ring} ${styles.ringB}`} aria-hidden="true" />
<span className={styles.badge}><CirclesThreeIcon /></span>
</div>
<p className={styles.copy}>{copy}</p>
<button
className={styles.btn}
type="button"
aria-pressed={on ? "true" : "false"}
onClick={() => setOn(!on)}
>
{on ? ctaOn : cta}
</button>
{/* 버튼을 누르면 이 한 줄이 바뀐다 — 실제 등록이 아니라 이 화면 안의 표시 */}
<p className={styles.live} role="status" aria-live="polite">{on ? noteOn : note}</p>
</section>
);
}09 Minimal logo hero — Measured area 7.18% · 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


