🧩 Design System3. Typography & Spacing03 — Radius · Shadow · z-index

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-none0강한 형태 (테이블·차트)
rounded-sm2px미세한 부드러움 (input·badge)
rounded (md)4pxdefault (button·card)
rounded-md6px약간 부드러운 (card)
rounded-lg8px부드러운 (modal·card)
rounded-xl12px매우 부드러운 (대형 surface)
rounded-2xl16px둥근 (toast·feature card)
rounded-3xl24px매우 둥근 (마케팅)
rounded-full9999px완전한 원·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예시 브랜드
Sharp0~2pxLinear, Vercel(과거), 엔터프라이즈 SaaS
Soft6~8pxGitHub, Stripe, 콘솔
Rounded12~16pxApple, Notion
Playful24~32pxDuolingo, 키즈 앱

모든 radius 토큰을 일괄로 키우는 한 줄 변경으로 브랜드 을 바꿀 수 있다 — 이게 토큰 시스템의 진짜 가치.


What — Shadow / Elevation scale

Material 3 elevation 모델

Material Design은 elevation을 dp 단위로 정의 — Y offset + blur가 함께 증가한다.

ElevationTailwindY offsetBlurspreadopacity용도
0shadow-none0000flat
1shadow-sm1200.05input hover, sticky bar
2shadow (md)1300.1card
3shadow-md46-10.1dropdown, popover
4shadow-lg1015-30.1modal
5shadow-xl2025-50.1drawer
6shadow-2xl2550-120.25dialog, hero card
insetshadow-inner24 inset0.05input 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)"  // 다크는 더 진한 그림자
      }
    }
  }
}

다크모드는 밝은 배경 위 진한 그림자가 더 자연스럽다 — opacity0.4~0.6 까지 올려야 시각적 elevation이 살아난다.


What — z-index stack tier

사실상 표준 stack (Bootstrap·Chakra·Material 공통)

Tierz-index용도
hide-1DOM에는 있지만 안 보임
base0default
docked10sticky bar, FAB
dropdown1000select, menu
sticky1100sticky header
banner1200top banner, cookie notice
overlay1300dimmed background
modal1400dialog, drawer
popover1500tooltip, popover (modal 위)
skipLink1600A11y skip link
toast1700notifications
tooltip1800가장 위 (Chakra는 toast 위)

핵심 규칙

  1. 100 단위로 띄워둠 — 충돌 시 사이에 끼울 여유 (1050 등).
  2. 9999 같은 수치 경쟁 금지 — 시스템 차원에서 해결.
  3. 의미로 호출z-modal, z-toast. 숫자 직접 사용 금지.
  4. 새 컴포넌트는 기존 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 추가MyCustomMenuz-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*까지만 쓰고 fullavatar·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, 24dp6단계. 각 단계의 그림자는 공식으로 자동 생성:

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의 @theme directive와 Panda의 tokens.{radii,shadows,zIndex}로 양쪽 노출.