01오프셋 그림자 히어로
제목 뒤에 깔린 색깔 복제본이 인쇄가 살짝 밀린 것처럼 주기적으로 어긋났다 맞물리고, 모서리 스티커 배지는 비뚤게 놓였다가 탁 소리 나듯 제자리로 돌아옵니다.
쓰임SaaS 랜딩 첫 화면, 캠페인 커버
오프셋 텍스트섀도우배지 회전→0radius 0

<main class="stage">
<section class="hero">
<span class="hero__badge is-demo" aria-hidden="true">NEW</span>
<div class="hero__card is-demo">
<p class="hero__eyebrow">SLABKIT</p>
<h1 class="hero__title is-demo" data-text="숨김 없는 시작">숨김 없는 시작</h1>
<p class="hero__desc">그림자까지 그대로 보여줄게요</p>
<button class="hero__cta" type="button">지금 시작하기</button>
</div>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 어긋남↔맞물림 한 주기
$easing: $ease-spring; // 어긋나는 곡선
$color: $stage-yellow; // 오프셋 복제 색
$border: 3px;
@include stage($stage-ink);
.hero {
position: relative;
width: clamp(260px, 84vw, 400px);
}
.hero__card {
box-sizing: border-box;
position: relative;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: $sp-2;
padding: $sp-4;
border: $border solid $subject-ink;
border-radius: 0;
background: $subject-cream;
box-shadow: $sp-2 $sp-2 0 $subject-ink;
}
.hero__badge {
box-sizing: border-box;
position: absolute;
top: -$sp-3;
right: -$sp-3;
z-index: 2;
border: $border solid $subject-ink;
border-radius: 0;
background: $stage-orange;
color: $subject-ink;
padding: $sp-1 $sp-3;
font-weight: 800;
font-size: clamp(10px, 2.4vw, 12px);
letter-spacing: .04em;
text-transform: uppercase;
animation: badgeSnap $duration $easing infinite;
}
.hero__eyebrow {
margin: 0;
color: $subject-ink;
opacity: .6;
font-weight: 800;
font-size: clamp(10px, 2.4vw, 11px);
line-height: 1;
letter-spacing: .08em;
text-transform: uppercase;
}
.hero__title {
position: relative;
margin: 0;
color: $subject-ink;
font-weight: 900;
font-size: clamp(22px, 6vw, 30px);
line-height: 1.1;
}
.hero__title::before {
content: attr(data-text);
position: absolute;
left: 0;
top: 0;
z-index: -1;
color: $color;
animation: ghostShift $duration $easing infinite;
}
.hero__desc {
margin: 0;
color: rgba($subject-ink, .72);
font-weight: 600;
font-size: clamp(12px, 2.8vw, 14px);
line-height: 1.4;
}
.hero__cta {
box-sizing: border-box;
margin-top: $sp-1;
border: $border solid $subject-ink;
border-radius: $r-pill;
background: $subject-ink;
color: $subject-cream;
padding: $sp-2 $sp-6;
font-weight: 800;
font-size: clamp(12px, 2.8vw, 14px);
box-shadow: $sp-1 $sp-1 0 $color;
}
// 데모: 제목 뒤 복제본이 인쇄 밀림처럼 어긋났다 맞물리고, 배지는 비뚤게 있다가 탁 소리 나듯 편다
@keyframes ghostShift {
0%, 100% { transform: translate($sp-1, $sp-1); }
50% { transform: translate(-$sp-1, -$sp-1); }
}
@keyframes badgeSnap {
0%, 55%, 100% { transform: rotate(-10deg); }
78% { transform: rotate(4deg); }
88% { transform: rotate(0deg); }
}
@media (prefers-reduced-motion: reduce) {
.hero__title::before, .hero__badge {
animation: none !important;
transform: none;
}
}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;
width: clamp(260px, 84vw, 400px);
}
.hero__card {
box-sizing: border-box;
position: relative;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
padding: 16px;
border: 3px solid #17141a;
border-radius: 0;
background: #fff7e6;
box-shadow: 8px 8px 0 #17141a;
}
.hero__badge {
box-sizing: border-box;
position: absolute;
top: -12px;
right: -12px;
z-index: 2;
border: 3px solid #17141a;
border-radius: 0;
background: #ff4d1f;
color: #17141a;
padding: 4px 12px;
font-weight: 800;
font-size: clamp(10px, 2.4vw, 12px);
letter-spacing: 0.04em;
text-transform: uppercase;
animation: badgeSnap 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.hero__eyebrow {
margin: 0;
color: #17141a;
opacity: 0.6;
font-weight: 800;
font-size: clamp(10px, 2.4vw, 11px);
line-height: 1;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.hero__title {
position: relative;
margin: 0;
color: #17141a;
font-weight: 900;
font-size: clamp(22px, 6vw, 30px);
line-height: 1.1;
}
.hero__title::before {
content: attr(data-text);
position: absolute;
left: 0;
top: 0;
z-index: -1;
color: #ffd23f;
animation: ghostShift 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.hero__desc {
margin: 0;
color: rgba(23, 20, 26, 0.72);
font-weight: 600;
font-size: clamp(12px, 2.8vw, 14px);
line-height: 1.4;
}
.hero__cta {
box-sizing: border-box;
margin-top: 4px;
border: 3px solid #17141a;
border-radius: 999px;
background: #17141a;
color: #fff7e6;
padding: 8px 24px;
font-weight: 800;
font-size: clamp(12px, 2.8vw, 14px);
box-shadow: 4px 4px 0 #ffd23f;
}
@keyframes ghostShift {
0%, 100% {
transform: translate(4px, 4px);
}
50% {
transform: translate(-4px, -4px);
}
}
@keyframes badgeSnap {
0%, 55%, 100% {
transform: rotate(-10deg);
}
78% {
transform: rotate(4deg);
}
88% {
transform: rotate(0deg);
}
}
@media (prefers-reduced-motion: reduce) {
.hero__title::before, .hero__badge {
animation: none !important;
transform: none;
}
}import type { CSSProperties } from "react";
import styles from "./HeroBrutalist.module.scss";
export type HeroBrutalistProps = {
eyebrow?: string;
title?: string;
desc?: string;
cta?: string;
badge?: string;
/** 한 주기(기본 2000ms) */
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function HeroBrutalist({
eyebrow = "SLABKIT",
title = "숨김 없는 시작",
desc = "그림자까지 그대로 보여줄게요",
cta = "지금 시작하기",
badge = "NEW",
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#ffd23f",
}: HeroBrutalistProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.hero} style={vars}>
<span className={styles.badge} aria-hidden="true">{badge}</span>
<div className={styles.card}>
<p className={styles.eyebrow}>{eyebrow}</p>
<h1 className={styles.title} data-text={title}>{title}</h1>
<p className={styles.desc}>{desc}</p>
<button type="button" className={styles.cta}>{cta}</button>
</div>
</section>
);
}01 오프셋 그림자 히어로 — 측정 누적면적 0.671% · 움직인 프레임 10/23GODRICH 원작 · zip 에 SCSS·React 포함 · 네오브루탈리즘 UI 랜딩 부품 9개, 그림자는 안 번진다
이 데모가 속한 글
네오브루탈리즘 UI 랜딩 부품 9개, 그림자는 안 번진다네오브루탈리즘 UI는 두꺼운 먹색 테두리와 번짐 없는 직각 그림자, 채도 높은 원색 블록으로 화면을 가감 없이 드러내는 디자인 스타일입니다. 이 글의 데모 전부 보기


