다크 SaaS 랜딩에 스며든 리니어 디자인 시스템 9종
핀터레스트를 뒤덮은 리니어 디자인 시스템은 이슈 트래커 Linear.app이 쓰는 짙은 잉크 배경에 파란 글로우 하나로 포인트를 주는 다크 스타일입니다.
이 다크 톤을 그대로 써서 조립한 원페이지 랜딩을 먼저 스크롤해 보십시오. 내비게이션에서 푸터까지 밝기가 한 번도 흔들리지 않는지가 핵심입니다.

위 화면을 만든 부품 아홉 개는 하나의 다크 모드 디자인 시스템으로 묶여 있고, 아래 격자에서 각각 따로 열어 볼 수 있습니다. 배열 순서는 방문자가 실제로 페이지를 훑는 흐름(첫눈에 들어오기, 둘러보기, 값 확인하기, 믿을 만한지 보기, 행동하기, 등록하기) 그대로입니다. 같은 다크 무드를 칸으로 나눠 담으면 벤토 그리드 랜딩 부품이 되고, 글자가 직접 움직이며 분위기를 끌면 키네틱 타이포그래피 랜딩이 이어집니다.
01 hero-linear
02 nav-linear
03 card-grid-linear
04 feature-list-linear
05 pricing-linear
06 testimonial-linear
07 cta-band-linear
08 footer-linear
09 buttons-inputs-linear
자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip
- 01 제품 목업+글로우 히어로
- 02 블러 스티키 내비
- 03 테두리 하이라이트 카드
- 04 모노스페이스 라벨 리스트
- 05 강조 플랜 글로우
- 06 로고 월+인용
- 07 그라디언트 보더 밴드
- 08 4열 다크 푸터
- 09 고스트/프라이머리 버튼
01히어로
목업 창과 파란 글로우
브라우저 크롬 목업 창을 짙은 잉크 배경 위에 띄우고, 그 뒤로 파란 글로우 하나만 은은하게 번지게 합니다.

<main class="stage">
<section class="mock is-demo">
<span class="mock__glow" aria-hidden="true"></span>
<div class="mock__bar">
<span class="mock__dot"></span>
<span class="mock__dot"></span>
<span class="mock__dot"></span>
</div>
<div class="mock__body">
<p class="mock__eyebrow">Console</p>
<h2 class="mock__title">한 화면에서 배포까지</h2>
<button class="mock__cta" type="button">무료로 시작</button>
</div>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 목업이 뜨고 가라앉는 한 주기
$easing: $ease-spring; // 등장 곡선
$color: $subject-blue; // 글로우 색
@include stage($stage-ink);
.mock {
position: relative;
width: clamp(220px, 78vw, 340px);
border-radius: $r-card;
background: $subject-ink;
border: 1px solid rgba($subject-cream, .08);
box-shadow: $shadow-raised;
overflow: hidden;
}
.mock__glow {
position: absolute;
left: 50%;
top: -20%;
width: 70%;
height: 140%;
transform: translateX(-50%);
background: radial-gradient(circle, rgba($color, .55) 0%, rgba($color, 0) 70%);
filter: blur(6px);
animation: glowPulse $duration $easing infinite;
pointer-events: none;
}
.mock__bar {
position: relative;
display: flex;
gap: $sp-2;
padding: clamp(8px, 2.4vw, $sp-3) clamp(12px, 3.2vw, $sp-4);
border-bottom: 1px solid rgba($subject-cream, .08);
}
.mock__dot {
width: $sp-2;
height: $sp-2;
border-radius: 50%;
background: rgba($subject-cream, .18);
}
.mock__body {
position: relative;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: clamp(8px, 2.4vw, $sp-3);
padding: clamp(16px, 6vw, $sp-8) clamp(12px, 4vw, $sp-6) clamp(20px, 7vw, $sp-10);
animation: bodyRise $duration $easing infinite;
}
.mock__eyebrow {
margin: 0;
color: $subject-blue-light; // 먹 면 위 파란 글자 — AA 4.5 (blog-design 글자 대비)
font: 700 clamp(10px, 2.6vw, 12px)/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
letter-spacing: .06em;
text-transform: uppercase;
}
.mock__title {
margin: 0;
color: $subject-cream;
font: 700 clamp(15px, 4.4vw, 22px)/1.2 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.mock__cta {
border: 0;
border-radius: $r-pill;
background: $subject-blue-deep;
color: $subject-cream;
padding: $sp-2 $sp-5;
font-weight: 600; font-size: clamp(11px, 2.8vw, 13px); line-height: 1;
}
@keyframes glowPulse {
0%, 100% { opacity: .45; transform: translateX(-50%) scale(.94); }
50% { opacity: .85; transform: translateX(-50%) scale(1.04); }
}
@keyframes bodyRise {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
}
@media (prefers-reduced-motion: reduce) {
.mock__glow, .mock__body { animation: none !important; opacity: .65; 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);
}
.mock {
position: relative;
width: clamp(220px, 78vw, 340px);
border-radius: 16px;
background: #17141a;
border: 1px solid rgba(255, 247, 230, 0.08);
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
overflow: hidden;
}
.mock__glow {
position: absolute;
left: 50%;
top: -20%;
width: 70%;
height: 140%;
transform: translateX(-50%);
background: radial-gradient(circle, rgba(47, 109, 246, 0.55) 0%, rgba(47, 109, 246, 0) 70%);
filter: blur(6px);
animation: glowPulse 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
pointer-events: none;
}
.mock__bar {
position: relative;
display: flex;
gap: 8px;
padding: clamp(8px, 2.4vw, 12px) clamp(12px, 3.2vw, 16px);
border-bottom: 1px solid rgba(255, 247, 230, 0.08);
}
.mock__dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: rgba(255, 247, 230, 0.18);
}
.mock__body {
position: relative;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: clamp(8px, 2.4vw, 12px);
padding: clamp(16px, 6vw, 32px) clamp(12px, 4vw, 24px) clamp(20px, 7vw, 40px);
animation: bodyRise 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.mock__eyebrow {
margin: 0;
color: #5b8cf8;
font: 700 clamp(10px, 2.6vw, 12px)/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.mock__title {
margin: 0;
color: #fff7e6;
font: 700 clamp(15px, 4.4vw, 22px)/1.2 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.mock__cta {
border: 0;
border-radius: 999px;
background: #0a4fe8;
color: #fff7e6;
padding: 8px 20px;
font-weight: 600;
font-size: clamp(11px, 2.8vw, 13px);
line-height: 1;
}
@keyframes glowPulse {
0%, 100% {
opacity: 0.45;
transform: translateX(-50%) scale(0.94);
}
50% {
opacity: 0.85;
transform: translateX(-50%) scale(1.04);
}
}
@keyframes bodyRise {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-6px);
}
}
@media (prefers-reduced-motion: reduce) {
.mock__glow, .mock__body {
animation: none !important;
opacity: 0.65;
transform: none;
}
}import type { CSSProperties } from "react";
import styles from "./HeroLinear.module.scss";
export type HeroLinearProps = {
eyebrow?: string;
title?: string;
cta?: string;
/** 한 주기(기본 2000ms) */
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function HeroLinear({
eyebrow = "Console",
title = "From one screen to deploy",
cta = "Start for free",
duration = 2000,
easing = "cubic-bezier(.2,1.4,.4,1)",
color = "#2f6df6",
}: HeroLinearProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.mock} style={vars}>
<span className={styles.glow} aria-hidden="true" />
<div className={styles.bar}>
<span className={styles.dot} />
<span className={styles.dot} />
<span className={styles.dot} />
</div>
<div className={styles.body}>
<p className={styles.eyebrow}>{eyebrow}</p>
<h2 className={styles.title}>{title}</h2>
<button type="button" className={styles.cta}>{cta}</button>
</div>
</section>
);
}01 · 제품 목업+글로우 히어로 — 측정 누적면적 24.265% · 움직인 프레임 18/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
시선이 목업 안으로
목업 안 제목·CTA 버튼이 함께 살짝 떠올랐다 가라앉아 시선을 목업 안으로 끌어옵니다.
<section class="relative overflow-hidden rounded-card border border-line bg-ink p-2 shadow-raised">
<span class="glow pointer-events-none absolute left-1/2 top-0 h-96 w-96
-translate-x-1/2"></span>
<div class="flex gap-1.5 px-3 py-2">
<span class="h-2.5 w-2.5 rounded-full bg-cream/20"></span>
<span class="h-2.5 w-2.5 rounded-full bg-cream/20"></span>
</div>
<div class="relative flex flex-col gap-3 px-6 pb-10 pt-8">
<p class="text-xs font-bold uppercase tracking-wide text-accent">Console</p>
<h2 class="text-2xl font-bold text-cream">한 화면에서 배포까지</h2>
<button class="w-fit rounded-pill bg-accent px-5 py-2 text-sm font-bold text-cream">
무료로 시작
</button>
</div>
</section>
02내비게이션
흐려진 캡슐 내비
backdrop-filter로 흐려진 캡슐형 내비 바 안에서, 현재 메뉴를 표시하는 작은 알약이 항목 사이를 옮겨 다닙니다.

<main class="stage">
<nav class="navbar">
<span class="navbar__brand">Vantage</span>
<div class="navbar__items is-demo">
<span class="navbar__pill" aria-hidden="true"></span>
<span class="navbar__item">홈</span>
<span class="navbar__item">기능</span>
<span class="navbar__item">요금제</span>
</div>
<button class="navbar__cta" type="button">시작</button>
</nav>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 필이 두 항목 사이를 오가는 한 주기
$easing: $ease-spring;
$color: $subject-blue;
@include stage($stage-yellow);
$item-gap: $sp-1;
.navbar {
--item-w: clamp(32px, 12vw, 64px); // 3개 항목이 320px 폭에서도 브랜드·CTA 와 함께 들어가도록 반응형
display: flex;
align-items: center;
gap: $sp-4;
width: clamp(240px, 84vw, 380px);
border-radius: $r-pill;
background: rgba($subject-ink, .92);
border: 1px solid rgba($subject-cream, .1);
box-shadow: $shadow-raised;
padding: $sp-2 $sp-2 $sp-2 $sp-4;
backdrop-filter: blur(10px);
}
.navbar__brand {
flex: 0 0 auto;
color: $subject-cream;
font: 700 clamp(12px, 3vw, 14px)/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.navbar__items {
position: relative;
display: flex;
gap: $item-gap;
flex: 1 1 auto;
background: rgba($subject-cream, .05);
border-radius: $r-pill;
padding: $sp-1;
}
.navbar__pill {
position: absolute;
left: $sp-1;
top: $sp-1;
bottom: $sp-1;
width: var(--item-w);
border-radius: $r-pill;
background: $subject-blue-deep;
animation: pillMove $duration $easing infinite;
}
.navbar__item {
position: relative;
z-index: 1;
flex: 0 0 var(--item-w);
width: var(--item-w);
text-align: center;
padding: $sp-2 0;
color: rgba($subject-cream, .9); // 파란 알약($subject-blue-deep) 위에서 .7 은 3.68:1 이었다 — AA 4.5 (5.10:1)
font-weight: 600; font-size: clamp(10.5px, 2.6vw, 12px); line-height: 1;
}
.navbar__cta {
flex: 0 0 auto;
border: 0;
border-radius: $r-pill;
background: $subject-blue-deep;
color: $subject-cream;
padding: $sp-2 $sp-4;
font-weight: 600; font-size: clamp(10.5px, 2.6vw, 12px); line-height: 1;
}
@keyframes pillMove {
0%, 20% { transform: translateX(0); }
50%, 70% { transform: translateX(calc(var(--item-w) + #{$item-gap})); }
100% { transform: translateX(0); }
}
@media (prefers-reduced-motion: reduce) {
.navbar__pill { animation: none !important; transform: translateX(0); }
}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);
}
.navbar {
--item-w: clamp(32px, 12vw, 64px);
display: flex;
align-items: center;
gap: 16px;
width: clamp(240px, 84vw, 380px);
border-radius: 999px;
background: rgba(23, 20, 26, 0.92);
border: 1px solid rgba(255, 247, 230, 0.1);
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
padding: 8px 8px 8px 16px;
backdrop-filter: blur(10px);
}
.navbar__brand {
flex: 0 0 auto;
color: #fff7e6;
font: 700 clamp(12px, 3vw, 14px)/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.navbar__items {
position: relative;
display: flex;
gap: 4px;
flex: 1 1 auto;
background: rgba(255, 247, 230, 0.05);
border-radius: 999px;
padding: 4px;
}
.navbar__pill {
position: absolute;
left: 4px;
top: 4px;
bottom: 4px;
width: var(--item-w);
border-radius: 999px;
background: #0a4fe8;
animation: pillMove 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.navbar__item {
position: relative;
z-index: 1;
flex: 0 0 var(--item-w);
width: var(--item-w);
text-align: center;
padding: 8px 0;
color: rgba(255, 247, 230, 0.9);
font-weight: 600;
font-size: clamp(10.5px, 2.6vw, 12px);
line-height: 1;
}
.navbar__cta {
flex: 0 0 auto;
border: 0;
border-radius: 999px;
background: #0a4fe8;
color: #fff7e6;
padding: 8px 16px;
font-weight: 600;
font-size: clamp(10.5px, 2.6vw, 12px);
line-height: 1;
}
@keyframes pillMove {
0%, 20% {
transform: translateX(0);
}
50%, 70% {
transform: translateX(calc(var(--item-w) + 4px));
}
100% {
transform: translateX(0);
}
}
@media (prefers-reduced-motion: reduce) {
.navbar__pill {
animation: none !important;
transform: translateX(0);
}
}import type { CSSProperties } from "react";
import styles from "./NavLinear.module.scss";
export type NavLinearProps = {
brand?: string;
items?: [string, string, string];
cta?: string;
/** 필이 두 항목 사이를 오가는 한 주기(기본 2000ms) */
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function NavLinear({
brand = "Vantage",
items = ["홈", "기능", "요금제"],
cta = "시작",
duration = 2000,
easing = "cubic-bezier(.2,1.4,.4,1)",
color = "#0a4fe8",
}: NavLinearProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<nav className={styles.navbar} style={vars}>
<span className={styles.brand}>{brand}</span>
<div className={styles.items}>
<span className={styles.pill} aria-hidden="true" />
{items.map((label) => (
<span key={label} className={styles.item}>{label}</span>
))}
</div>
<button type="button" className={styles.cta}>{cta}</button>
</nav>
);
}02 · 블러 스티키 내비 — 측정 누적면적 2.219% · 움직인 프레임 15/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
브랜드와 CTA 자리
브랜드와 CTA 버튼은 가운데 메뉴 캡슐 바깥, 내비 바 양 끝에 고정합니다.
<nav class="flex items-center gap-4 rounded-pill bg-ink/90 border border-line
backdrop-blur px-3 py-2 shadow-raised">
<span class="pl-3 font-display font-extrabold text-sm text-cream">Vantage</span>
<div class="relative flex flex-1 rounded-pill bg-cream/5 p-1">
<span class="absolute inset-y-1 left-1 w-1/4 rounded-pill bg-accent"></span>
<span class="relative z-10 flex-1 text-center text-xs font-semibold py-2 text-cream">홈</span>
<span class="relative z-10 flex-1 text-center text-xs py-2 text-cream/70">기능</span>
</div>
<button class="rounded-pill bg-accent px-4 py-2 text-xs font-bold text-cream">시작</button>
</nav>
03카드 격자
한 칸씩 순서대로 밝아진다
다크 카드 격자에서 한 칸씩 순서대로 테두리가 파란 빛으로 밝아졌다 가라앉으며 시선을 이끕니다.

<main class="stage">
<section class="grid is-demo">
<article class="grid__card grid__card--1">
<span class="grid__dot" aria-hidden="true"></span>
<p class="grid__label">자동 배포</p>
</article>
<article class="grid__card grid__card--2">
<span class="grid__dot" aria-hidden="true"></span>
<p class="grid__label">이슈 추적</p>
</article>
<article class="grid__card grid__card--3">
<span class="grid__dot" aria-hidden="true"></span>
<p class="grid__label">로드맵</p>
</article>
<article class="grid__card grid__card--4">
<span class="grid__dot" aria-hidden="true"></span>
<p class="grid__label">API 연동</p>
</article>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 카드 한 장이 밝아졌다 가라앉는 한 주기
$easing: $ease-out;
$color: $subject-blue;
@include stage($stage-orange);
.grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: $sp-3;
width: clamp(220px, 76vw, 320px);
}
.grid__card {
border-radius: $r-card;
background: $subject-ink;
border: 1px solid rgba($subject-cream, .08);
padding: $sp-4;
display: flex;
flex-direction: column;
gap: $sp-4;
animation-name: cardGlow;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.grid__card--1 { animation-delay: 0s; }
.grid__card--2 { animation-delay: $duration * -0.75; }
.grid__card--3 { animation-delay: $duration * -0.5; }
.grid__card--4 { animation-delay: $duration * -0.25; }
.grid__dot {
width: $sp-4;
height: $sp-4;
border-radius: $r-xs;
background: rgba($color, .35);
}
.grid__label {
margin: 0;
color: $subject-cream;
font: 600 clamp(11px, 2.8vw, 13px)/1.3 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
@keyframes cardGlow {
0%, 76%, 100% { border-color: rgba($subject-cream, .08); box-shadow: none; }
13% { border-color: $color; box-shadow: 0 0 0 1px rgba($color, .5), $shadow-raised; }
}
@media (prefers-reduced-motion: reduce) {
.grid__card { animation: none !important; border-color: rgba($subject-cream, .08); box-shadow: none; }
}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);
}
.grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
width: clamp(220px, 76vw, 320px);
}
.grid__card {
border-radius: 16px;
background: #17141a;
border: 1px solid rgba(255, 247, 230, 0.08);
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
animation-name: cardGlow;
animation-duration: 2s;
animation-timing-function: ease-out;
animation-iteration-count: infinite;
}
.grid__card--1 {
animation-delay: 0s;
}
.grid__card--2 {
animation-delay: -1.5s;
}
.grid__card--3 {
animation-delay: -1s;
}
.grid__card--4 {
animation-delay: -0.5s;
}
.grid__dot {
width: 16px;
height: 16px;
border-radius: 4px;
background: rgba(47, 109, 246, 0.35);
}
.grid__label {
margin: 0;
color: #fff7e6;
font: 600 clamp(11px, 2.8vw, 13px)/1.3 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
@keyframes cardGlow {
0%, 76%, 100% {
border-color: rgba(255, 247, 230, 0.08);
box-shadow: none;
}
13% {
border-color: #2f6df6;
box-shadow: 0 0 0 1px rgba(47, 109, 246, 0.5), 0 14px 24px rgba(23, 20, 26, 0.32);
}
}
@media (prefers-reduced-motion: reduce) {
.grid__card {
animation: none !important;
border-color: rgba(255, 247, 230, 0.08);
box-shadow: none;
}
}import type { CSSProperties } from "react";
import styles from "./CardGridLinear.module.scss";
export type CardGridLinearProps = {
labels?: [string, string, string, string];
/** 카드 한 장이 밝아졌다 가라앉는 한 주기(기본 2000ms) */
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function CardGridLinear({
labels = ["자동 배포", "이슈 추적", "로드맵", "API 연동"],
duration = 2000,
easing = "ease-out",
color = "#2f6df6",
}: CardGridLinearProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
const delays = ["0s", `${-duration * 0.75}ms`, `${-duration * 0.5}ms`, `${-duration * 0.25}ms`];
return (
<section className={styles.grid} style={vars}>
{labels.map((label, i) => (
<article key={label} className={styles.card} style={{ animationDelay: delays[i] }}>
<span className={styles.dot} aria-hidden="true" />
<p className={styles.label}>{label}</p>
</article>
))}
</section>
);
}03 · 테두리 하이라이트 카드 — 측정 누적면적 12.578% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
동시에 반짝이지 않는 이유
격자 전체가 한꺼번에 반짝이지 않고 한 칸씩 순서대로 도는 것이 핵심입니다.
<div class="grid grid-cols-2 gap-gutter">
<div class="rounded-card border border-line bg-cream/[.03] p-6
transition-colors hover:border-accent">
<span class="mb-3 block h-4 w-4 rounded-sm bg-accent/40"></span>
<p class="text-sm font-semibold text-cream">자동 배포</p>
</div>
<div class="rounded-card border border-line bg-cream/[.03] p-6
transition-colors hover:border-accent">
<span class="mb-3 block h-4 w-4 rounded-sm bg-accent/40"></span>
<p class="text-sm font-semibold text-cream">이슈 추적</p>
</div>
</div>
04기능 목록
모노스페이스 번호 라벨
각 기능 앞에 모노스페이스 번호 라벨을 달고, 리스트 줄마다 아래에서 위로 스치듯 나타납니다.

<main class="stage">
<ul class="list is-demo">
<li class="list__row list__row--1"><span class="list__num">01</span><span class="list__text">실시간 협업 편집</span></li>
<li class="list__row list__row--2"><span class="list__num">02</span><span class="list__text">키보드 전용 커맨드바</span></li>
<li class="list__row list__row--3"><span class="list__num">03</span><span class="list__text">버전별 변경 로그</span></li>
</ul>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 줄 하나가 스치듯 나타났다 사라지는 한 주기
$easing: $ease-out;
$color: $subject-blue;
@include stage($stage-paper);
.list {
list-style: none;
margin: 0;
padding: $sp-4;
display: flex;
flex-direction: column;
gap: $sp-2;
width: clamp(220px, 76vw, 320px);
background: $subject-ink;
border-radius: $r-card;
box-shadow: $shadow-raised;
}
.list__row {
display: flex;
align-items: center;
gap: $sp-4;
padding: $sp-3 $sp-4;
border-radius: $r-sm;
// 데모 루프는 .is-demo 안에서만 — 정적(실사용) 상태는 opacity 1 이라야 글자 대비가 산다
.list.is-demo & { animation-name: rowSweep; }
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.list__row--1 { animation-delay: 0s; }
.list__row--2 { animation-delay: $duration * -0.67; }
.list__row--3 { animation-delay: $duration * -0.34; }
.list__num {
font: 700 clamp(11px, 2.8vw, 13px)/1 "Courier New", ui-monospace, monospace;
color: $subject-blue-light; // 먹 면 위 파란 글자 — AA 4.5 (blog-design 글자 대비)
}
.list__text {
color: $subject-cream;
font: 600 clamp(11px, 2.8vw, 13px)/1.3 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
@keyframes rowSweep {
0%, 62%, 100% { opacity: .55; background: transparent; transform: translateY(0); }
17% { opacity: 1; background: rgba($color, .12); transform: translateY(-2px); }
}
@media (prefers-reduced-motion: reduce) {
.list__row { animation: none !important; opacity: .85; background: transparent; transform: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.list {
list-style: none;
margin: 0;
padding: 16px;
display: flex;
flex-direction: column;
gap: 8px;
width: clamp(220px, 76vw, 320px);
background: #17141a;
border-radius: 16px;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
}
.list__row {
display: flex;
align-items: center;
gap: 16px;
padding: 12px 16px;
border-radius: 8px;
}
.list.is-demo .list__row {
animation-name: rowSweep;
}
.list__row {
animation-duration: 2s;
animation-timing-function: ease-out;
animation-iteration-count: infinite;
}
.list__row--1 {
animation-delay: 0s;
}
.list__row--2 {
animation-delay: -1.34s;
}
.list__row--3 {
animation-delay: -0.68s;
}
.list__num {
font: 700 clamp(11px, 2.8vw, 13px)/1 "Courier New", ui-monospace, monospace;
color: #5b8cf8;
}
.list__text {
color: #fff7e6;
font: 600 clamp(11px, 2.8vw, 13px)/1.3 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
@keyframes rowSweep {
0%, 62%, 100% {
opacity: 0.55;
background: transparent;
transform: translateY(0);
}
17% {
opacity: 1;
background: rgba(47, 109, 246, 0.12);
transform: translateY(-2px);
}
}
@media (prefers-reduced-motion: reduce) {
.list__row {
animation: none !important;
opacity: 0.85;
background: transparent;
transform: none;
}
}import type { CSSProperties } from "react";
import styles from "./FeatureListLinear.module.scss";
export type FeatureListLinearProps = {
rows?: [string, string, string];
/** 줄 하나가 스치듯 나타났다 사라지는 한 주기(기본 2000ms) */
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function FeatureListLinear({
rows = ["실시간 협업 편집", "키보드 전용 커맨드바", "버전별 변경 로그"],
duration = 2000,
easing = "ease-out",
color = "#2f6df6",
}: FeatureListLinearProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
const delays = ["0s", `${-duration * 0.67}ms`, `${-duration * 0.34}ms`];
return (
<ul className={styles.list} style={vars}>
{rows.map((text, i) => (
<li key={text} className={styles.row} style={{ animationDelay: delays[i] }}>
<span className={styles.num}>{String(i + 1).padStart(2, "0")}</span>
<span className={styles.text}>{text}</span>
</li>
))}
</ul>
);
}04 · 모노스페이스 라벨 리스트 — 측정 누적면적 27.006% · 움직인 프레임 19/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
코드 에디터 같은 인상
번호가 코드 에디터의 줄 번호처럼 보여 개발자 도구라는 인상을 강화합니다.
<ul class="flex flex-col gap-2 rounded-card border border-line bg-cream/[.03] p-4">
<li class="flex items-center gap-4 rounded-control px-4 py-3 hover:bg-accent/10">
<span class="font-mono text-xs font-bold text-accent">01</span>
<span class="text-sm font-semibold text-cream">실시간 협업 편집</span>
</li>
<li class="flex items-center gap-4 rounded-control px-4 py-3 hover:bg-accent/10">
<span class="font-mono text-xs font-bold text-accent">02</span>
<span class="text-sm font-semibold text-cream">키보드 전용 커맨드바</span>
</li>
</ul>
05가격
추천 플랜만 숨 쉰다
추천 플랜 카드에만 파란 글로우 테두리를 두르고, 은은하게 커졌다 작아지며 숨 쉬게 합니다.

<main class="stage">
<section class="plan is-demo">
<span class="plan__badge">추천</span>
<p class="plan__name">프로</p>
<p class="plan__price">월 19,000원</p>
<button class="plan__cta" type="button">선택하기</button>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 글로우 테두리가 숨 쉬는 한 주기
$easing: $ease-spring;
$color: $subject-blue;
@include stage($stage-ink);
.plan {
position: relative;
width: clamp(180px, 60vw, 220px);
border-radius: $r-card;
background: $subject-ink;
border: 2px solid $color;
padding: $sp-8 $sp-6 $sp-6;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: $sp-2;
animation: planBreathe $duration $easing infinite;
}
.plan__badge {
position: absolute;
top: $sp-1 * -2;
left: $sp-6;
border-radius: $r-pill;
background: $subject-blue-deep;
color: $subject-cream;
padding: $sp-1 $sp-3;
font: 700 clamp(9.5px, 2.4vw, 11px)/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.plan__name {
margin: 0;
color: $subject-cream;
font: 700 clamp(14px, 3.6vw, 17px)/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.plan__price {
margin: 0;
color: rgba($subject-cream, .7);
font-weight: 600; font-size: clamp(11px, 2.8vw, 13px); line-height: 1;
}
.plan__cta {
margin-top: $sp-2;
border: 0;
border-radius: $r-pill;
background: $subject-blue-deep;
color: $subject-cream;
padding: $sp-2 $sp-5;
font-weight: 600; font-size: clamp(11px, 2.8vw, 13px); line-height: 1;
}
@keyframes planBreathe {
0%, 100% { box-shadow: 0 0 0 0 rgba($color, .35), $shadow-raised; transform: scale(1); }
50% { box-shadow: 0 0 0 10px rgba($color, 0), $shadow-raised; transform: scale(1.015); }
}
@media (prefers-reduced-motion: reduce) {
.plan { animation: none !important; box-shadow: 0 0 0 4px rgba($color, .2), $shadow-raised; 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);
}
.plan {
position: relative;
width: clamp(180px, 60vw, 220px);
border-radius: 16px;
background: #17141a;
border: 2px solid #2f6df6;
padding: 32px 24px 24px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
animation: planBreathe 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.plan__badge {
position: absolute;
top: -8px;
left: 24px;
border-radius: 999px;
background: #0a4fe8;
color: #fff7e6;
padding: 4px 12px;
font: 700 clamp(9.5px, 2.4vw, 11px)/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.plan__name {
margin: 0;
color: #fff7e6;
font: 700 clamp(14px, 3.6vw, 17px)/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.plan__price {
margin: 0;
color: rgba(255, 247, 230, 0.7);
font-weight: 600;
font-size: clamp(11px, 2.8vw, 13px);
line-height: 1;
}
.plan__cta {
margin-top: 8px;
border: 0;
border-radius: 999px;
background: #0a4fe8;
color: #fff7e6;
padding: 8px 20px;
font-weight: 600;
font-size: clamp(11px, 2.8vw, 13px);
line-height: 1;
}
@keyframes planBreathe {
0%, 100% {
box-shadow: 0 0 0 0 rgba(47, 109, 246, 0.35), 0 14px 24px rgba(23, 20, 26, 0.32);
transform: scale(1);
}
50% {
box-shadow: 0 0 0 10px rgba(47, 109, 246, 0), 0 14px 24px rgba(23, 20, 26, 0.32);
transform: scale(1.015);
}
}
@media (prefers-reduced-motion: reduce) {
.plan {
animation: none !important;
box-shadow: 0 0 0 4px rgba(47, 109, 246, 0.2), 0 14px 24px rgba(23, 20, 26, 0.32);
transform: none;
}
}import type { CSSProperties } from "react";
import styles from "./PricingLinear.module.scss";
export type PricingLinearProps = {
badge?: string;
name?: string;
price?: string;
cta?: string;
/** 글로우 테두리가 숨 쉬는 한 주기(기본 2000ms) */
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function PricingLinear({
badge = "추천",
name = "프로",
price = "$19/mo",
cta = "선택하기",
duration = 2000,
easing = "cubic-bezier(.2,1.4,.4,1)",
color = "#2f6df6",
}: PricingLinearProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.plan} style={vars}>
<span className={styles.badge}>{badge}</span>
<p className={styles.name}>{name}</p>
<p className={styles.price}>{price}</p>
<button type="button" className={styles.cta}>{cta}</button>
</section>
);
}05 · 강조 플랜 글로우 — 측정 누적면적 6.678% · 움직인 프레임 19/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
표 없이 밀어주는 법
표 없이도 어느 플랜을 밀고 있는지 한눈에 들어옵니다.
<div class="relative flex-[1.1] rounded-card border-2 border-accent
bg-cream/[.03] p-6 pt-8 shadow-glow">
<span class="absolute -top-3 left-6 rounded-pill bg-accent px-3 py-1
text-xs font-bold text-cream">추천</span>
<p class="font-bold text-cream">프로</p>
<p class="mt-1 text-sm text-cream/60">월 19,000원</p>
</div>
06후기
옅게 지운 로고 줄
옅게 지운 로고 줄 아래 인용 카드 하나를 두고, 로고들이 차례로 밝아졌다 옅어지며 신뢰를 쌓습니다.

<main class="stage">
<section class="panel">
<div class="panel__logos is-demo" aria-hidden="true">
<span class="panel__logo panel__logo--1"></span>
<span class="panel__logo panel__logo--2"></span>
<span class="panel__logo panel__logo--3"></span>
<span class="panel__logo panel__logo--4"></span>
</div>
<blockquote class="panel__quote">
<p>“온보딩 문서 없이도 팀 전체가 하루 만에 옮겨 왔어요.”</p>
<cite>이하늘 · 디자인 리드</cite>
</blockquote>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 로고 한 개가 밝아졌다 옅어지는 한 주기
$easing: $ease-out;
$color: $subject-blue;
@include stage($stage-yellow);
.panel {
width: clamp(230px, 80vw, 340px);
border-radius: $r-card;
background: $subject-ink;
border: 1px solid rgba($subject-cream, .08);
box-shadow: $shadow-raised;
padding: $sp-6;
display: flex;
flex-direction: column;
gap: $sp-5;
}
.panel__logos {
display: flex;
justify-content: space-between;
gap: $sp-3;
}
.panel__logo {
flex: 1 1 0;
height: $sp-4;
border-radius: $r-xs;
background: rgba($subject-cream, .16);
animation-name: logoShine;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.panel__logo--1 { animation-delay: 0s; }
.panel__logo--2 { animation-delay: $duration * -0.75; }
.panel__logo--3 { animation-delay: $duration * -0.5; }
.panel__logo--4 { animation-delay: $duration * -0.25; }
.panel__quote {
margin: 0;
padding-top: $sp-4;
border-top: 1px solid rgba($subject-cream, .08);
}
.panel__quote p {
margin: 0 0 $sp-2;
color: $subject-cream;
font: 600 clamp(12px, 3vw, 14px)/1.5 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.panel__quote cite {
color: rgba($subject-cream, .55);
font-weight: 600; font-size: clamp(10.5px, 2.6vw, 12px); line-height: 1;
font-style: normal;
}
@keyframes logoShine {
0%, 76%, 100% { background: rgba($subject-cream, .16); }
13% { background: $color; }
}
@media (prefers-reduced-motion: reduce) {
.panel__logo { animation: none !important; background: rgba($subject-cream, .16); }
}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);
}
.panel {
width: clamp(230px, 80vw, 340px);
border-radius: 16px;
background: #17141a;
border: 1px solid rgba(255, 247, 230, 0.08);
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
padding: 24px;
display: flex;
flex-direction: column;
gap: 20px;
}
.panel__logos {
display: flex;
justify-content: space-between;
gap: 12px;
}
.panel__logo {
flex: 1 1 0;
height: 16px;
border-radius: 4px;
background: rgba(255, 247, 230, 0.16);
animation-name: logoShine;
animation-duration: 2s;
animation-timing-function: ease-out;
animation-iteration-count: infinite;
}
.panel__logo--1 {
animation-delay: 0s;
}
.panel__logo--2 {
animation-delay: -1.5s;
}
.panel__logo--3 {
animation-delay: -1s;
}
.panel__logo--4 {
animation-delay: -0.5s;
}
.panel__quote {
margin: 0;
padding-top: 16px;
border-top: 1px solid rgba(255, 247, 230, 0.08);
}
.panel__quote p {
margin: 0 0 8px;
color: #fff7e6;
font: 600 clamp(12px, 3vw, 14px)/1.5 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.panel__quote cite {
color: rgba(255, 247, 230, 0.55);
font-weight: 600;
font-size: clamp(10.5px, 2.6vw, 12px);
line-height: 1;
font-style: normal;
}
@keyframes logoShine {
0%, 76%, 100% {
background: rgba(255, 247, 230, 0.16);
}
13% {
background: #2f6df6;
}
}
@media (prefers-reduced-motion: reduce) {
.panel__logo {
animation: none !important;
background: rgba(255, 247, 230, 0.16);
}
}import type { CSSProperties } from "react";
import styles from "./TestimonialLinear.module.scss";
export type TestimonialLinearProps = {
quote?: string;
cite?: string;
logoCount?: number;
/** 로고 한 개가 밝아졌다 옅어지는 한 주기(기본 2000ms) */
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function TestimonialLinear({
quote = "Our whole team switched over in a day, no onboarding docs needed.",
cite = "Lee Ha-neul · Design Lead",
logoCount = 4,
duration = 2000,
easing = "ease-out",
color = "#2f6df6",
}: TestimonialLinearProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
const delays = Array.from({ length: logoCount }, (_, i) => `${-duration * (0.75 - i * 0.25)}ms`);
return (
<section className={styles.panel} style={vars}>
<div className={styles.logos} aria-hidden="true">
{delays.map((delay, i) => (
<span key={i} className={styles.logo} style={{ animationDelay: i === 0 ? "0s" : delay }} />
))}
</div>
<blockquote className={styles.quote}>
<p>“{quote}”</p>
<cite>{cite}</cite>
</blockquote>
</section>
);
}06 · 로고 월+인용 — 측정 누적면적 3.367% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
왜 16% 밝기인가
로고를 완전한 색으로 두면 광고 배너처럼 보여, 일부러 16% 밝기로 죽였습니다.
<div class="rounded-card border border-line bg-cream/[.03] p-6">
<div class="mb-5 flex justify-between gap-3 opacity-60">
<span class="h-4 flex-1 rounded-sm bg-cream/20"></span>
<span class="h-4 flex-1 rounded-sm bg-cream/20"></span>
</div>
<blockquote class="border-t border-line pt-4">
<p class="text-sm font-semibold text-cream">
“온보딩 문서 없이도 팀 전체가 하루 만에 옮겨 왔어요.”
</p>
</blockquote>
</div>
07CTA 띠
회전하는 그라디언트 선
CTA 띠 테두리를 감싸는 그라디언트 선이 천천히 회전해, 끝단이 빛나는 배지처럼 보입니다.

<main class="stage">
<section class="band">
<div class="band__inner is-demo">
<h3 class="band__title">지금 팀을 옮겨 보세요</h3>
<button class="band__cta" type="button">무료로 시작</button>
</div>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 테두리 그라디언트가 한 바퀴 도는 시간
$easing: linear; // 회전은 일정 속도가 자연스럽다 (spring·pop 은 회전에 안 맞는다)
$color: $subject-blue;
@include stage($stage-orange);
.band {
position: relative;
width: clamp(240px, 84vw, 360px);
border-radius: $r-card;
padding: $sp-1;
overflow: hidden;
isolation: isolate;
}
.band::before {
content: "";
position: absolute;
inset: -40%;
background: conic-gradient(from 0deg, transparent 0%, rgba($color, .95) 18%, transparent 40%);
animation: bandSpin $duration $easing infinite;
}
.band__inner {
position: relative;
z-index: 1;
border-radius: calc(#{$r-card} - $sp-1);
background: $subject-ink;
padding: $sp-8 $sp-6;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: $sp-4;
}
.band__title {
margin: 0;
color: $subject-cream;
font: 700 clamp(15px, 3.8vw, 19px)/1.3 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.band__cta {
border: 0;
border-radius: $r-pill;
background: $subject-blue-deep;
color: $subject-cream;
padding: $sp-2 $sp-5;
font-weight: 600; font-size: clamp(11px, 2.8vw, 13px); line-height: 1;
}
@keyframes bandSpin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
.band::before { animation: none !important; }
}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);
}
.band {
position: relative;
width: clamp(240px, 84vw, 360px);
border-radius: 16px;
padding: 4px;
overflow: hidden;
isolation: isolate;
}
.band::before {
content: "";
position: absolute;
inset: -40%;
background: conic-gradient(from 0deg, transparent 0%, rgba(47, 109, 246, 0.95) 18%, transparent 40%);
animation: bandSpin 2s linear infinite;
}
.band__inner {
position: relative;
z-index: 1;
border-radius: calc(16px - 4px);
background: #17141a;
padding: 32px 24px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.band__title {
margin: 0;
color: #fff7e6;
font: 700 clamp(15px, 3.8vw, 19px)/1.3 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.band__cta {
border: 0;
border-radius: 999px;
background: #0a4fe8;
color: #fff7e6;
padding: 8px 20px;
font-weight: 600;
font-size: clamp(11px, 2.8vw, 13px);
line-height: 1;
}
@keyframes bandSpin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: reduce) {
.band::before {
animation: none !important;
}
}import type { CSSProperties } from "react";
import styles from "./CtaBandLinear.module.scss";
export type CtaBandLinearProps = {
title?: string;
cta?: string;
/** 테두리 그라디언트가 한 바퀴 도는 시간(기본 2000ms) */
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function CtaBandLinear({
title = "Move your team over today",
cta = "Start for free",
duration = 2000,
easing = "linear",
color = "#2f6df6",
}: CtaBandLinearProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.band} style={vars}>
<div className={styles.inner}>
<h3 className={styles.title}>{title}</h3>
<button type="button" className={styles.cta}>{cta}</button>
</div>
</section>
);
}07 · 그라디언트 보더 밴드 — 측정 누적면적 2.754% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
2px만 남기는 방법
conic-gradient를 박스보다 크게 깔고 회전만 시킨 뒤 안쪽 카드로 2px만 남기고 가려서 만듭니다.
<div class="relative overflow-hidden rounded-card border border-accent/40
bg-cream/[.03] px-10 py-12 text-center shadow-glow">
<h3 class="mx-auto max-w-md text-3xl font-extrabold text-cream">
지금 팀을 옮겨 보세요
</h3>
<button class="mt-6 rounded-pill bg-accent px-6 py-3 text-sm font-bold text-cream">
무료로 시작
</button>
</div>
08푸터
왼쪽부터 떠오르는 링크
4열로 나뉜 링크 묶음이 왼쪽부터 순서대로 옅게 떠오르며 어두운 푸터를 채웁니다.

<main class="stage">
<footer class="footer is-demo">
<div class="footer__col footer__col--1">
<p class="footer__head">제품</p>
<p class="footer__link">기능</p>
</div>
<div class="footer__col footer__col--2">
<p class="footer__head">회사</p>
<p class="footer__link">소개</p>
</div>
<div class="footer__col footer__col--3">
<p class="footer__head">리소스</p>
<p class="footer__link">문서</p>
</div>
<div class="footer__col footer__col--4">
<p class="footer__head">고객지원</p>
<p class="footer__link">문의</p>
</div>
</footer>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 열 하나가 떠오르는 한 주기
$easing: $ease-out;
$color: $subject-blue;
@include stage($stage-paper);
.footer {
box-sizing: border-box; // width 에 padding 을 포함시켜 320px 에서 넘치지 않게
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: $sp-4;
width: clamp(240px, 86vw, 360px);
background: $subject-ink;
border-radius: $r-card;
padding: $sp-6;
}
.footer__col {
display: flex;
flex-direction: column;
gap: $sp-2;
// 데모 루프는 .is-demo 안에서만 — 정적(실사용) 상태는 opacity 1 이라야 글자 대비가 산다
.footer.is-demo & { animation-name: colRise; }
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.footer__col--1 { animation-delay: 0s; }
.footer__col--2 { animation-delay: $duration * -0.75; }
.footer__col--3 { animation-delay: $duration * -0.5; }
.footer__col--4 { animation-delay: $duration * -0.25; }
.footer__head {
margin: 0;
color: $subject-blue-light; // 먹 면 위 파란 글자 — AA 4.5 (blog-design 글자 대비)
font: 700 clamp(9px, 2.2vw, 10.5px)/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
text-transform: uppercase;
letter-spacing: .04em;
}
.footer__link {
margin: 0;
color: rgba($subject-cream, .7);
font-weight: 600; font-size: clamp(10px, 2.4vw, 11.5px); line-height: 1.4;
}
@keyframes colRise {
0%, 62%, 100% { opacity: .35; transform: translateY(4px); }
17% { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
.footer__col { animation: none !important; opacity: 1; transform: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.footer {
box-sizing: border-box;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
width: clamp(240px, 86vw, 360px);
background: #17141a;
border-radius: 16px;
padding: 24px;
}
.footer__col {
display: flex;
flex-direction: column;
gap: 8px;
}
.footer.is-demo .footer__col {
animation-name: colRise;
}
.footer__col {
animation-duration: 2s;
animation-timing-function: ease-out;
animation-iteration-count: infinite;
}
.footer__col--1 {
animation-delay: 0s;
}
.footer__col--2 {
animation-delay: -1.5s;
}
.footer__col--3 {
animation-delay: -1s;
}
.footer__col--4 {
animation-delay: -0.5s;
}
.footer__head {
margin: 0;
color: #5b8cf8;
font: 700 clamp(9px, 2.2vw, 10.5px)/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.footer__link {
margin: 0;
color: rgba(255, 247, 230, 0.7);
font-weight: 600;
font-size: clamp(10px, 2.4vw, 11.5px);
line-height: 1.4;
}
@keyframes colRise {
0%, 62%, 100% {
opacity: 0.35;
transform: translateY(4px);
}
17% {
opacity: 1;
transform: translateY(0);
}
}
@media (prefers-reduced-motion: reduce) {
.footer__col {
animation: none !important;
opacity: 1;
transform: none;
}
}import type { CSSProperties } from "react";
import styles from "./FooterLinear.module.scss";
export type FooterLinearProps = {
columns?: { head: string; link: string }[];
/** 열 하나가 떠오르는 한 주기(기본 2000ms) */
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const DEFAULT_COLUMNS = [
{ head: "제품", link: "기능" },
{ head: "회사", link: "소개" },
{ head: "리소스", link: "문서" },
{ head: "고객지원", link: "문의" },
];
export default function FooterLinear({
columns = DEFAULT_COLUMNS,
duration = 2000,
easing = "ease-out",
color = "#2f6df6",
}: FooterLinearProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
const delays = columns.map((_, i) => `${-duration * (0.75 - i * 0.25)}ms`);
return (
<footer className={styles.footer} style={vars}>
{columns.map((col, i) => (
<div key={col.head} className={styles.col} style={{ animationDelay: i === 0 ? "0s" : delays[i] }}>
<p className={styles.head}>{col.head}</p>
<p className={styles.link}>{col.link}</p>
</div>
))}
</footer>
);
}08 · 4열 다크 푸터 — 측정 누적면적 1.578% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
구분선 없이 나누기
구분선 없이 열 사이 여백만으로 영역을 나눕니다.
<footer class="grid grid-cols-4 gap-gutter border-t border-line py-10">
<div>
<p class="mb-2 text-xs font-bold uppercase tracking-wide text-accent">제품</p>
<p class="text-sm text-cream/70">기능</p>
</div>
<div>
<p class="mb-2 text-xs font-bold uppercase tracking-wide text-accent">회사</p>
<p class="text-sm text-cream/70">소개</p>
</div>
</footer>
09버튼·입력
다크 모드 ui 디자인에서 무엇이 움직이나
프라이머리 버튼의 파란 글로우와 입력창의 포커스 링 모양 테두리는 호버나 포커스와 상관없이 자동으로 커졌다 작아지길 반복하고, 고스트 버튼만 테두리만 두른 채 움직이지 않습니다.

<main class="stage">
<form class="form is-demo">
<label class="form__field">
<input class="form__input" type="email" placeholder="이메일 주소" readonly>
</label>
<div class="form__row">
<button class="form__ghost" type="button">더 알아보기</button>
<button class="form__primary" type="button">무료로 시작</button>
</div>
</form>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 글로우·포커스 링이 커졌다 작아지는 한 주기
$easing: $ease-out;
$color: $subject-blue;
@include stage($stage-ink);
.form {
display: flex;
flex-direction: column;
gap: $sp-4;
width: clamp(210px, 74vw, 280px);
}
.form__field {
display: block;
border-radius: $r-control;
background: rgba($subject-cream, .05);
border: 1px solid rgba($subject-cream, .12);
}
.form__input {
width: 100%;
box-sizing: border-box;
background: transparent;
border: 0;
outline: none;
padding: $sp-3 $sp-4;
color: $subject-cream;
font: 600 clamp(11px, 2.8vw, 13px)/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
border-radius: $r-control;
animation: inputRing $duration $easing infinite;
}
.form__row {
display: flex;
gap: $sp-3;
}
.form__ghost {
flex: 1 1 0;
border-radius: $r-control;
background: transparent;
border: 1px solid rgba($subject-cream, .2);
color: $subject-cream;
padding: $sp-3 $sp-4;
font-weight: 600; font-size: clamp(10.5px, 2.6vw, 12px); line-height: 1;
}
.form__primary {
flex: 1 1 0;
border-radius: $r-control;
border: 0;
background: $subject-blue-deep;
color: $subject-cream;
padding: $sp-3 $sp-4;
font-weight: 600; font-size: clamp(10.5px, 2.6vw, 12px); line-height: 1;
animation: btnGlow $duration $easing infinite;
}
@keyframes inputRing {
0%, 100% { box-shadow: 0 0 0 0 rgba($color, 0); }
50% { box-shadow: 0 0 0 3px rgba($color, .45); }
}
@keyframes btnGlow {
0%, 100% { box-shadow: 0 0 0 0 rgba($color, 0); }
50% { box-shadow: 0 6px 18px rgba($color, .55); }
}
@media (prefers-reduced-motion: reduce) {
.form__input, .form__primary { animation: none !important; box-shadow: 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);
}
.form {
display: flex;
flex-direction: column;
gap: 16px;
width: clamp(210px, 74vw, 280px);
}
.form__field {
display: block;
border-radius: 12px;
background: rgba(255, 247, 230, 0.05);
border: 1px solid rgba(255, 247, 230, 0.12);
}
.form__input {
width: 100%;
box-sizing: border-box;
background: transparent;
border: 0;
outline: none;
padding: 12px 16px;
color: #fff7e6;
font: 600 clamp(11px, 2.8vw, 13px)/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
border-radius: 12px;
animation: inputRing 2s ease-out infinite;
}
.form__row {
display: flex;
gap: 12px;
}
.form__ghost {
flex: 1 1 0;
border-radius: 12px;
background: transparent;
border: 1px solid rgba(255, 247, 230, 0.2);
color: #fff7e6;
padding: 12px 16px;
font-weight: 600;
font-size: clamp(10.5px, 2.6vw, 12px);
line-height: 1;
}
.form__primary {
flex: 1 1 0;
border-radius: 12px;
border: 0;
background: #0a4fe8;
color: #fff7e6;
padding: 12px 16px;
font-weight: 600;
font-size: clamp(10.5px, 2.6vw, 12px);
line-height: 1;
animation: btnGlow 2s ease-out infinite;
}
@keyframes inputRing {
0%, 100% {
box-shadow: 0 0 0 0 rgba(47, 109, 246, 0);
}
50% {
box-shadow: 0 0 0 3px rgba(47, 109, 246, 0.45);
}
}
@keyframes btnGlow {
0%, 100% {
box-shadow: 0 0 0 0 rgba(47, 109, 246, 0);
}
50% {
box-shadow: 0 6px 18px rgba(47, 109, 246, 0.55);
}
}
@media (prefers-reduced-motion: reduce) {
.form__input, .form__primary {
animation: none !important;
box-shadow: none;
}
}import type { CSSProperties } from "react";
import styles from "./ButtonsInputsLinear.module.scss";
export type ButtonsInputsLinearProps = {
placeholder?: string;
ghostLabel?: string;
primaryLabel?: string;
/** 글로우·포커스 링이 커졌다 작아지는 한 주기(기본 2000ms) */
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function ButtonsInputsLinear({
placeholder = "Email address",
ghostLabel = "Learn more",
primaryLabel = "Start for free",
duration = 2000,
easing = "ease-out",
color = "#2f6df6",
}: ButtonsInputsLinearProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<form className={styles.form} style={vars}>
<label className={styles.field}>
<input className={styles.input} type="email" placeholder={placeholder} readOnly />
</label>
<div className={styles.row}>
<button type="button" className={styles.ghost}>{ghostLabel}</button>
<button type="button" className={styles.primary}>{primaryLabel}</button>
</div>
</form>
);
}09 · 고스트/프라이머리 버튼 — 측정 누적면적 4.033% · 움직인 프레임 17/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
카드와 같은 모서리 반경
세 요소의 모서리 반경을 카드와 같은 값으로 맞춰 화면 전체가 한 규격으로 보이게 합니다.
<form class="mx-auto flex max-w-md flex-col gap-3 rounded-card border
border-line bg-cream/[.03] p-6 sm:flex-row">
<label class="relative flex-1 rounded-control border border-line bg-cream/5">
<input class="w-full rounded-control bg-transparent px-4 py-3 text-sm
text-cream outline-none" type="email" placeholder="이메일 주소">
</label>
<button class="rounded-control bg-accent px-5 py-3 text-sm font-bold text-cream">
가입하기
</button>
</form>
tailwind.config 커스텀
설정 하나만 읽는다
위에서 본 부품 아홉 개와 원페이지는 전부 아래 설정 하나만 읽습니다.
톤이 한 번에 바뀐다
배경·강조·반경·그림자 값을 여기서 고치면 화면 전체 톤이 한 번에 따라 바뀝니다.
theme: {
extend: {
colors: {
ink: "#17141a",
cream: "#fff7e6",
accent: "#2f6df6",
line: "rgba(255,247,230,.08)",
},
fontFamily: {
display: ["'Inter'", "system-ui", "sans-serif"],
body: ["'Inter'", "system-ui", "sans-serif"],
},
borderRadius: { control: "12px", card: "16px", pill: "999px" },
boxShadow: {
raised: "0 14px 24px rgba(23,20,26,.32)",
glow: "0 0 40px rgba(47,109,246,.35)",
},
spacing: { gutter: "16px" },
},
},
바이브코딩 프롬프트
짧게 지시해도 되는 이유
위 설정 값을 프로젝트에 붙여 넣은 다음, Claude Code나 Cursor에는 아래처럼 짧게 지시하면 충분합니다. 전체 프롬프트 원문은 zip의 prompt.md에 들어 있습니다.
배경은 ink(#17141a) 하나. 카드도 순백이 아니라 ink 위에 옅은 cream 오버레이(3~5%)로 만든다.
강조가 필요한 곳(버튼·추천 배지·포커스 링)에만 accent(#2f6df6)를 쓴다.
두 번째 강조색을 넣지 않는다. 반경은 rounded-card·rounded-pill·rounded-control 세 값만 쓴다.
돌려 본 결과
이 지시를 그대로 page/index.html에 넣어 돌린 결과는 1280px 뷰포트 기준 전체 페이지 캡처로 남겼습니다.

어디서 깨지는가 — 함정
theme() 가 조용히 무시된다
이 스타일에서 가장 흔한 함정은 배경을 theme() 함수로 지정하는 것입니다. Tailwind Play CDN은 컴파일된 유틸리티 클래스(bg-ink 같은) 안의 theme() 값은 정상 처리하지만, <style> 태그 안에 직접 쓴 background: theme('colors.ink')는 무시하고 기본값(흰 배경)으로 조용히 되돌아갑니다. 이 프로젝트도 처음엔 <body> 배경을 <style> 태그의 theme()로 지정했다가 원페이지 전체가 흰 배경으로 렌더돼, 가격 카드의 text-cream/60 텍스트가 흰 배경 위에서 거의 안 보이는 상태로 스크린샷에 남을 뻔했습니다. <body class="bg-ink">처럼 컴파일되는 유틸리티 클래스로 바꾸고서야 잉크 배경이 제대로 적용됐습니다. 어두운 면이 밝은 텍스트를 삼키는 현상은 색이 반대로 뒤집히는 트랜잭셔널 이메일 템플릿에서 다크모드 미지원으로 되풀이됩니다. 두 번째 함정은 모서리 반경입니다. 카드마다 rounded-card·rounded-pill·rounded-control을 벗어난 임의의 px 값을 쓰면 화면 전체가 한 시스템이 아니라 여러 사이트를 이어붙인 것처럼 보입니다. 부품 아홉 개와 원페이지 소스를 통째로 담은 zip은 암호가 걸려 있는데, 그 암호는 wdu6yd23이며 본문 아래 버튼을 누르면 내려받을 수 있습니다.
320px 좁은 화면
세 번째 함정은 320px처럼 좁은 화면입니다. 01번 히어로 목업의 안쪽 여백을 고정 px로 주면 좁은 화면에서 카드 높이가 뷰포트를 넘겨 스크롤바가 생기는데, 여백을 clamp()로 바꿔야 폭에 따라 자연스럽게 줄어듭니다.
접근성
동작 줄이기에서는 정지 상태로
아홉 개 데모 전부 MDN prefers-reduced-motion 문서가 정의한 미디어 쿼리로 감쌌고, 동작 줄이기를 켠 방문자에게는 회전·글로우·스침 같은 움직임 대신 정지된 최종 상태만 보입니다.
색 말고 텍스트로도
02번 내비게이션의 필과 06번의 로고 블록처럼 색으로만 상태를 표시하는 요소는 aria-hidden 처리하고, 실제 의미(현재 메뉴, 파트너명)는 항상 곁에 텍스트로 함께 둡니다.
미지원 브라우저와 더 읽을 거리
02번은 MDN backdrop-filter 문서의 안내대로 미지원 브라우저에서도 반투명 배경만으로 텍스트 대비가 유지되게 만들었습니다.
이 편 말고 다른 트렌드가 궁금하면 디자인 트렌드 카테고리를 훑어보시고, 이 사이트 자체가 어떤 곳인지는 소개 페이지에 짧게 적어 뒀습니다.
FAQ
리니어(Linear) 로고나 상표를 그대로 써도 되나요?
아니요. 이 편의 부품은 Linear.app의 로고·상표·실제 화면을 복제한 것이 아니라 짙은 배경·파란 강조·모노스페이스 라벨 같은 일반적인 디자인 언어만 참고했습니다. 브랜드명은 전부 가상의 이름(Vantage)으로 바꿔 실었습니다.
다크 모드 웹 디자인을 라이트 모드와 같이 써도 되나요?
가능합니다. ink와 cream의 역할만 서로 바꾸면 되지만, accent 하나로 시선을 모으는 이 스타일의 핵심은 배경이 밝든 어둡든 그대로 유지돼야 합니다. 강조색을 두 개 이상 쓰면 어느 요소가 중요한지 알 수 없어집니다.
Tailwind 없이 순수 CSS로도 쓸 수 있나요?
네, zip 안의 tokens.css 하나만 링크하면 됩니다. 이 파일은 tailwind.config.js와 같은 값을 CSS 변수(--color-ink, --color-accent 등)로 옮겨 둔 것이라, Tailwind 클래스 대신 var(--color-accent)처럼 직접 참조하면 같은 톤을 유지할 수 있습니다. 할인 문구와 마감 시간까지 얹은 판촉 구간이 필요하면 프로모션 랜딩 섹션 템플릿이 그 조립도입니다.









