03 — Radius · Shadow · z-index
이 문서가 답하는 질문: 디자인 시스템의 부드러움(radius), 깊이(shadow), *층(z-index)*은 각각 어떻게 토큰화하는가? 왜
z-index: 9999를 쓰면 안 되는가? 한 줄 답 (Pyramid Top): 이 셋은 모두 의미적 tier다 — radius는 5~7단의 부드러움 단계, shadow는 5~7단의 elevation, z-index는 base·dropdown·sticky·overlay·modal·toast·tooltip의 7단 stack. 셋 다 닫힌 집합이고, 임의의 중간값을 만들면 즉시 깨진다.
Why — 왜 의미 tier인가
1) Radius — “왜 이 모서리는 6, 저건 4?”
border-radius: 6px이 디자이너 직관으로 정한 값이라면, 다음 카드에서는 4px이 더 자연스러워 보일 수도 있다. 결정 기준이 없으면 매번 회의가 필요하다.
2) Shadow — 깊이는 수치가 아니라 역할
box-shadow: 0 2px 4px rgba(0,0,0,0.1)을 그대로 외울 수는 없다. 디자이너의 머릿속엔 “낮은·중간·높은 elevation” 뿐이다.
3) z-index — 9999 지옥
.modal { z-index: 9999; }
.tooltip { z-index: 99999; } /* modal 위에 떠야 함 */
.toast { z-index: 999999; } /* tooltip 위에... */
.dev-tools { z-index: 9999999; } /* ??? */수치 경쟁이 시작되면 끝이 없다. stack tier가 답.
| 풀려는 문제 | tier 기반의 해법 | 효과 |
|---|---|---|
| radius 결정 기준 부재 | none·sm·md·lg·xl·full 6단 | 매번 회의 없음 |
| shadow의 수치 외우기 | elevation.1..5 semantic | 디자이너=개발자 동일 어휘 |
| z-index 9999 지옥 | base·dropdown·modal·toast | 충돌 시 시스템 차원에서 해결 |
How — 세 가지 tier의 생성 규칙
- Radius: 0 → full 사이를 반-기하급수로 (sm 2, md 6, lg 12, xl 24, 2xl 32).
- Shadow: y-offset과 blur가 함께 증가하는 5~7단의 elevation.
- z-index: 100 단위로 띄워둠 (충돌 시 사이에 끼울 여유).
What — Radius scale
Tailwind v4 default
| 토큰 | 값 | 용도 |
|---|---|---|
rounded-none | 0 | 강한 형태 (테이블·차트) |
rounded-sm | 2px | 미세한 부드러움 (input·badge) |
rounded (md) | 4px | default (button·card) |
rounded-md | 6px | 약간 부드러운 (card) |
rounded-lg | 8px | 부드러운 (modal·card) |
rounded-xl | 12px | 매우 부드러운 (대형 surface) |
rounded-2xl | 16px | 둥근 (toast·feature card) |
rounded-3xl | 24px | 매우 둥근 (마케팅) |
rounded-full | 9999px | 완전한 원·pill (avatar·chip) |
v4는
--radius-sm,--radius-md등 CSS variable로 노출 — 다크모드나 테마별 radius 변경도 가능.
Panda CSS
theme: {
tokens: {
radii: {
none: { value: "0" },
sm: { value: "0.125rem" }, // 2px
md: { value: "0.375rem" }, // 6px
lg: { value: "0.5rem" }, // 8px
xl: { value: "0.75rem" }, // 12px
"2xl":{ value: "1rem" }, // 16px
"3xl":{ value: "1.5rem" }, // 24px
full: { value: "9999px" }
}
},
semanticTokens: {
radii: {
"control": { value: "{radii.md}" }, // input·button
"card": { value: "{radii.lg}" },
"modal": { value: "{radii.xl}" },
"pill": { value: "{radii.full}" }
}
}
}- Semantic 층(
control,card)이 컴포넌트 의도를 표현. - 브랜드를 둥근 분위기로 바꾸려면
radii.control = {radii.lg}로 alias 변경 한 줄.
Brand radius personality
| 분위기 | base radius | 예시 브랜드 |
|---|---|---|
| Sharp | 0~2px | Linear, Vercel(과거), 엔터프라이즈 SaaS |
| Soft | 6~8px | GitHub, Stripe, 콘솔 |
| Rounded | 12~16px | Apple, Notion |
| Playful | 24~32px | Duolingo, 키즈 앱 |
모든 radius 토큰을 일괄로 키우는 한 줄 변경으로 브랜드 톤을 바꿀 수 있다 — 이게 토큰 시스템의 진짜 가치.
What — Shadow / Elevation scale
Material 3 elevation 모델
Material Design은 elevation을 dp 단위로 정의 — Y offset + blur가 함께 증가한다.
| Elevation | Tailwind | Y offset | Blur | spread | opacity | 용도 |
|---|---|---|---|---|---|---|
| 0 | shadow-none | 0 | 0 | 0 | 0 | flat |
| 1 | shadow-sm | 1 | 2 | 0 | 0.05 | input hover, sticky bar |
| 2 | shadow (md) | 1 | 3 | 0 | 0.1 | card |
| 3 | shadow-md | 4 | 6 | -1 | 0.1 | dropdown, popover |
| 4 | shadow-lg | 10 | 15 | -3 | 0.1 | modal |
| 5 | shadow-xl | 20 | 25 | -5 | 0.1 | drawer |
| 6 | shadow-2xl | 25 | 50 | -12 | 0.25 | dialog, hero card |
| inset | shadow-inner | 2 | 4 inset | — | 0.05 | input field depth |
Tailwind default shadow 값
@theme {
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1),
0 1px 2px -1px rgb(0 0 0 / 0.1);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1),
0 2px 4px -2px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1),
0 4px 6px -4px rgb(0 0 0 / 0.1);
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1),
0 8px 10px -6px rgb(0 0 0 / 0.1);
--shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);
}Panda CSS
theme: {
tokens: {
shadows: {
sm: { value: "0 1px 2px 0 rgb(0 0 0 / 0.05)" },
md: { value: "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)" },
lg: { value: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)" },
xl: { value: "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)" },
"2xl": { value: "0 25px 50px -12px rgb(0 0 0 / 0.25)" }
}
},
semanticTokens: {
shadows: {
"card": { value: "{shadows.sm}" },
"popover": { value: "{shadows.md}" },
"modal": { value: "{shadows.lg}" },
"dialog": { value: "{shadows.xl}" }
}
}
}Dark mode shadow
semanticTokens: {
shadows: {
"card": {
value: {
base: "0 1px 2px 0 rgb(0 0 0 / 0.05)",
_dark: "0 1px 2px 0 rgb(0 0 0 / 0.4)" // 다크는 더 진한 그림자
}
}
}
}다크모드는 밝은 배경 위 진한 그림자가 더 자연스럽다 —
opacity를 0.4~0.6 까지 올려야 시각적 elevation이 살아난다.
What — z-index stack tier
사실상 표준 stack (Bootstrap·Chakra·Material 공통)
| Tier | z-index | 용도 |
|---|---|---|
hide | -1 | DOM에는 있지만 안 보임 |
base | 0 | default |
docked | 10 | sticky bar, FAB |
dropdown | 1000 | select, menu |
sticky | 1100 | sticky header |
banner | 1200 | top banner, cookie notice |
overlay | 1300 | dimmed background |
modal | 1400 | dialog, drawer |
popover | 1500 | tooltip, popover (modal 위) |
skipLink | 1600 | A11y skip link |
toast | 1700 | notifications |
tooltip | 1800 | 가장 위 (Chakra는 toast 위) |
핵심 규칙
- 100 단위로 띄워둠 — 충돌 시 사이에 끼울 여유 (
1050등). - 9999 같은 수치 경쟁 금지 — 시스템 차원에서 해결.
- 의미로 호출 —
z-modal,z-toast. 숫자 직접 사용 금지. - 새 컴포넌트는 기존 tier에 끼움 —
Tooltip on top of Modal같은 케이스는 stack 정책 문서화.
Panda CSS
theme: {
tokens: {
zIndex: {
hide: { value: "-1" },
base: { value: "0" },
docked: { value: "10" },
dropdown: { value: "1000" },
sticky: { value: "1100" },
banner: { value: "1200" },
overlay: { value: "1300" },
modal: { value: "1400" },
popover: { value: "1500" },
skipLink: { value: "1600" },
toast: { value: "1700" },
tooltip: { value: "1800" }
}
}
}Tailwind (v4)
@theme {
--z-hide: -1;
--z-base: 0;
--z-docked: 10;
--z-dropdown: 1000;
--z-sticky: 1100;
--z-banner: 1200;
--z-overlay: 1300;
--z-modal: 1400;
--z-popover: 1500;
--z-toast: 1700;
--z-tooltip: 1800;
}<div class="z-modal">...</div>What-if — 잘못 쓰면 어떻게 깨지는가
- 함정 1: 새 컴포넌트가 임의의 z-index 추가 —
MyCustomMenu가z-index: 9998를 씀. 이후Modal(1400) 위에 얹히지 않거나, 다른Popover(1500) 가 밀려남. 대응: 모든 z-index 토큰을 강제 검수(ESLint custom rule). 정 필요하면 tier 추가 RFC. - 함정 2: stacking context 무지 —
z-index는 부모의position+transform+opacity < 1등에 의해 새 stacking context가 생기면 상위 z-index가 무력화됨. 대응: stacking context 발생 조건을 문서화 (MDN: stacking context). - 함정 3: shadow 색상이 black 고정 — 브랜드 색이 파란색인데 그림자가 검정 → 차갑게 보임. 대응: shadow 토큰에 브랜드 색 alpha 섞기 (
box-shadow: 0 4px 8px rgb(33 80 245 / 0.08)). - 함정 4: radius
9999px를 큰 element에 사용 — 카드 크기 400×200에border-radius: 9999px을 주면 반원이 됨 (의도 아님).rounded-full은 높이=너비일 때만 동작. 대응: card에는 *최대2xl/3xl*까지만 쓰고full은 avatar·chip에만. - 함정 5: dark mode에서 shadow 안 보임 — 밝은 그림자가 어두운 배경에서 안 보임. 대응: semantic token에서 base/dark 따로 정의.
Insight — Material의 dp 모델이 사실상 표준이 된 이유
Material Design(2014)이 등장하기 전, 디자이너들은 box-shadow를 즉흥적으로 만들었다. Y-offset, blur, spread, color 4개 변수에 수치적 직관이 없었다.
Material은 “elevation은 dp(density-independent pixel) 단위의 높이” 라고 정의했다 — 1dp, 2dp, 4dp, 8dp, 16dp, 24dp 의 6단계. 각 단계의 그림자는 공식으로 자동 생성:
key shadow: y = elevation, blur = elevation × 1, opacity = 0.14
ambient shadow: y = elevation/2, blur = elevation × 1.5, opacity = 0.12이게 디자이너의 직관(이 카드는 8dp 떠 있어)을 수치적 정의(box-shadow 4개 인자)로 번역한 첫 시스템이다.
Tailwind의 shadow-sm/md/lg/xl/2xl 5단도 Material의 6단을 단순화한 변형이다. 광원 위치는 위에서 약간 앞쪽이라는 물리적 모델도 그대로 차용했다 (그래서 blur가 항상 y보다 큼).
흥미로운 점: iOS의 “Liquid Glass” 디자인 언어(2024~)는 이 모델을 역행해 그림자를 거의 없애고 blur·반투명으로 깊이를 표현한다. 이제 일부 시스템(Apple HIG, 일부 Linear UI)은 shadow tier를 줄이고 backdrop-filter tier를 늘리는 방향으로 진화 중이다.
요약
- Radius·Shadow·z-index 모두 의미 tier다 — 5~7단의 닫힌 집합.
- z-index는 100 단위로 띄워서 충돌 여유를 둔다.
9999는 금지. - Shadow는 Material의 elevation 모델을 사실상 표준으로 사용.
- 다크모드는 shadow opacity를 더 진하게 — semantic token으로 분기.
- 같은 토큰을 Tailwind v4의
@themedirective와 Panda의tokens.{radii,shadows,zIndex}로 양쪽 노출.