5. 디자인 토큰
여러 파일이 공유하는 값은 전역 토큰으로 정의하고 사용처에서는 토큰 이름을 참조합니다. z-index 순서와 테마 값도 토큰으로 관리해 변경할 곳을 한곳에 모읍니다.
5.1 대체값은 공통 토큰 밖의 var()에만 둡니다
공통 토큰의 수정 위치를 하나로 유지하고 대체값의 중복을 막습니다.
대체값을 붙이는 자리
항상 주입되는 공통 토큰 목록을 :root나 전역 테마 스타일시트 한 곳에 선언합니다.
var()에 대체값을 붙일지는 그 목록과 대조해 정합니다.
flowchart LR
q1{"공통 토큰 목록에<br>있는 변수인가?"} -- 아니요 --> q2{"목록 밖 같은 변수를<br>두 곳 이상에서 쓰는가?"} -- 아니요 --> r3("사용처에 대체값")
q1 -- 예 --> r1("대체값 없이 씀")
q2 -- 예 --> r2("우리 토큰에 대체값 한 번<br>사용처는 그 토큰 참조")| 변수 | 대체값 |
|---|---|
| 공통 토큰 목록에 있음 | 쓰지 않습니다. 모든 테마에서 선언을 보장하고 이름을 목록과 대조합니다 |
| 그 밖의 변수 | 씁니다. 외부 라이브러리 변수나 실행 중 주입되는 수치처럼 값이 없을 수 있습니다 |
| 목록 밖의 같은 변수를 두 곳 이상에서 씀 | 대체값을 우리 토큰에 한 번만 적고 사용처는 그 토큰을 가리킵니다 |
공통 토큰에 대체값을 반복하면 선언이 빠져도 알아채기 어렵고 예전 값을 여러 곳에서 관리하게 됩니다. 이 규칙을 적용하려고 요청에 없는 CSS 변수를 만들지는 않습니다.
대체값이 쓰이는 때
대체값은 변수의 계산값을 사용할 수 없을 때 적용되며, 정상 주입된 토큰 값이 대체값보다 우선합니다.
| 상황 | 결과 |
|---|---|
미등록 변수가 없거나 initial로 초기화됨, 순환 참조로 무효가 됨 | 대체값을 사용합니다 |
--color: 12px처럼 값은 있지만 소비 속성 문법에 맞지 않음 | 대체값을 사용하지 않습니다 |
@property로 등록한 변수 | 등록 문법과 초기값이 먼저 적용되므로 그 계약을 확인합니다 |
| 대체값 없이 변수가 무효이거나 소비 속성 문법에 맞지 않음 | 앞선 선언으로 돌아가지 않습니다. 상속 속성은 상속값, 나머지는 초기값이 됩니다. color는 부모 색, z-index는 auto가 됩니다 |
공통 토큰에 대체값을 붙여 값을 두 곳에 둡니다.
.pg_orderFilterDialog__panel {
gap: var(--app-space-inline, 12px);
color: var(--app-color-text-primary, #212529);
}공통 토큰 목록에 있는 변수는 대체값 없이 씁니다.
/* src/style/token.css: 공통 토큰 목록의 단일 출처 */
:root {
--app-space-inline: 12px;
--app-color-text-primary: #212529;
}
/* src/page/orders/_pg-order-filter-dialog.css */
.pg_orderFilterDialog__panel {
gap: var(--app-space-inline);
color: var(--app-color-text-primary);
}
주입이 보장되지 않는 변수를 대체값 없이 씁니다.
.pg_orderFilterDialog__collapse {
& .MuiAccordion-root {
border-radius: var(--mui-shape-borderRadius);
}
}
목록에 없는 변수에는 대체값을 붙입니다.
.pg_orderFilterDialog__collapse {
& .MuiAccordion-root {
border-radius: var(--mui-shape-borderRadius, 10px);
}
}
함께 보기
5.2 여러 파일이 쓰는 값은 전역 토큰으로 씁니다
여러 파일이 쓰는 값은 전역 토큰으로 모으고 나머지는 선언 자리에 그대로 둡니다.
값을 둘 자리는 파일 경계로 판정하고 아래 예외를 함께 확인합니다.
flowchart LR
q1{"여러 파일에서<br>쓰는 값인가?"} -- 아니요 --> q2{"테마 색, 그림자,<br>z-index, 움직임 값인가?"} -- 아니요 --> q3{"실행 중에만<br>계산하는 수치인가?"} -- 아니요 --> r4("선언 자리에 값 그대로")
q1 -- 예 --> r1("전역 공통 토큰")
q2 -- 예 --> r2("전역 공통 토큰")
q3 -- 예 --> r3("지역 변수 하나를<br>TSX에서 전달")| 값의 범위나 역할 | 처리 |
|---|---|
| 여러 파일에서 사용함 | 기존 공통 토큰을 씁니다. 없으면 토큰 파일에 선언합니다 |
| 한 파일 안에서만 사용함 | 값을 그대로 둡니다 |
| 테마를 켠 프로젝트의 색과 그림자 | 한 파일에서만 써도 규칙 5.4에 따라 토큰으로 둡니다 |
z-index 층, 움직임 지속 시간과 이징 | 한 번만 써도 토큰으로 둡니다. 앱 전체의 쌓임 순서와 움직임 리듬을 맞춥니다 |
| 실행 중에만 계산할 수 있는 수치 하나 | 유일한 지역 변수 예외입니다. 규칙 3.6에 따라 TSX에서 전달합니다 |
그 밖의 지역 변수는 만들지 않습니다. 공통 토큰이 아니면 대체값이 필요해 사용처의 값은 남고 참조만 늘어납니다. 조상 상태는 변수 대신 결합자 하나로 자손에 전달하며, 결합자 범위는 규칙 2.3를 따릅니다. 층 목록은 규칙 5.3, 새 토큰 이름은 규칙 5.5가 정합니다.
한 파일 안 반복을 조상에 선언한 지역 변수로 감쌉니다.
.pg_products__root {
--pg-products-gap: 12px;
}
.pg_products__toolbar {
gap: var(--pg-products-gap, 12px);
}
.pg_products__footer {
gap: var(--pg-products-gap, 12px);
}
한 파일 안 반복은 값을 그대로 둡니다.
.pg_products__toolbar {
gap: 12px;
}
.pg_products__footer {
gap: 12px;
}
상태를 전달하려고 지역 변수를 만듭니다.
.pg_products__rowBadge {
border-color: var(--pg-products-row-accent);
}
.pg_products__row {
--pg-products-row-accent: transparent;
&:hover {
--pg-products-row-accent: #1677ff;
}
}
상태 전달은 지역 변수 없이 결합자 하나로 풉니다.
.pg_products__rowBadge {
border: 1px solid transparent;
}
.pg_products__row {
&:hover .pg_products__rowBadge {
border-color: #1677ff;
}
}
여러 파일이 쓰는 값을 각 파일에 하드코딩합니다.
.pg_products__row {
background: #f5f5f5;
}
/* pg-product-detail.css */
.pg_productDetail__row {
background: #f5f5f5;
}여러 파일이 쓰는 값은 전역 공통 토큰으로 둡니다.
:root {
--app-color-fill-muted: #f5f5f5;
}
/* pg-products.css */
.pg_products__row {
background: var(--app-color-fill-muted);
}
/* pg-product-detail.css */
.pg_productDetail__row {
background: var(--app-color-fill-muted);
}함께 보기
5.3 z-index 층은 토큰 파일 한 곳에 선언합니다
층 순서를 한 파일에서 확인하고 z-index 숫자를 임의로 늘리지 않습니다.
층 토큰
층은 전역 토큰 파일에 한 번 선언하고 z-index에서는 토큰 이름만 씁니다.
숫자를 직접 쓰거나 사용처에서 층 사이 값을 만들지 않습니다.
| 토큰 | 값 | 용도 |
|---|---|---|
--app-z-index-base | 0 | 일반 흐름 |
--app-z-index-sticky | 100 | sticky 헤더, 툴바 |
--app-z-index-overlay | 200 | 모달, 드로어, 백드롭 |
--app-z-index-popper | 300 | 툴팁, 드롭다운, 알림 |
새 용도가 네 층에 모두 맞지 않을 때만 토큰 파일에 층을 추가하고 100 간격을 유지합니다.
쌓임 맥락
층 순서는 같은 쌓임 맥락 안에서만 성립합니다.
조상의 맥락이 바깥 sticky보다 아래면 내부 popper의 숫자를 올려도 그 위로 나오지 못합니다.
| 새 쌓임 맥락을 만드는 대표 속성 | 조건 |
|---|---|
position | relative 또는 absolute이면서 z-index가 auto가 아님 |
transform, filter, backdrop-filter | none이 아님 |
will-change | 쌓임 맥락을 만드는 속성을 지정함 |
opacity, isolation, contain | opacity는 1 미만, isolation은 isolate, contain은 layout, paint, content, strict 중 하나 |
fixed와 sticky는 그 자체로 새 쌓임 맥락을 만듭니다.
가려졌을 때 확인 순서
요소가 가려졌으면 숫자를 올리기 전에 아래 순서로 확인합니다.
z-index가 적용되는지 봅니다. 일반 요소는static이면 적용되지 않고relative부터 적용됩니다.flex와grid아이템인지 봅니다.static이어도z-index에auto가 아닌 값을 주면 적용되고 쌓임 맥락도 만듭니다.- 같은 층 안에서 순서가 충돌하면 층 분류를 다시 봅니다.
값을
+1하지 않습니다. - 조상의 쌓임 맥락이나 잘림이 원인이면 해당 조상 밖의 포털 대상으로 옮깁니다. 포털도 실제 부착 위치의 DOM 맥락을 따르므로 대상 위치를 확인합니다.
showModal()로 연dialog나 열린 popover는 최상위 레이어입니다. 일반 문서의z-index토큰으로 그 위에 올라가려 하지 않습니다.
숫자를 직접 쓰고 다른 요소보다 하나씩 올립니다.
.pg_products__toolbar {
position: sticky;
z-index: 10;
}
/* src/component/widget/product-filter/wg-product-filter.css */
.wg_productFilter__dropdown {
position: absolute;
z-index: 11;
}층 토큰만 씁니다.
:root {
--app-z-index-base: 0;
--app-z-index-sticky: 100;
--app-z-index-overlay: 200;
--app-z-index-popper: 300;
}
/* src/page/products/pg-products.css */
.pg_products__toolbar {
/* 화면 스크롤 컨테이너 기준 고정. 드롭다운이 이 쌓임 맥락을 벗어나야 하면 포털 대상을 밖에 둠 */
position: sticky;
z-index: var(--app-z-index-sticky);
}
/* src/component/widget/product-filter/wg-product-filter.css */
.wg_productFilter__dropdown {
position: absolute;
z-index: var(--app-z-index-popper);
}함께 보기
5.4 테마는 토큰 값만 바꿔서 전환합니다
테마 분기가 한 파일에만 있어 색을 하나 더할 때 파일 여러 개를 열지 않습니다.
테마는 토큰 파일에서 값만 바꿉니다.
컴포넌트 CSS에는 prefers-color-scheme이나 [data-theme] 분기를 두지 않습니다.
flowchart LR
t1(":root 기본값") --> t2("@media<br>prefers-color-scheme") --> t3(":root[data-theme]")| 테마 조건이나 값 | 처리 |
|---|---|
| 시스템 테마 | 토큰 파일의 @media (prefers-color-scheme)에서 :root 값을 바꿉니다 |
| 사용자가 고른 테마 | [data-theme] 블록을 시스템 조건 뒤에 둡니다. 뒤에 오고 명시도도 높은 선택자가 시스템 설정을 덮습니다 |
| 같은 팔레트를 두 번 선언함 | 토큰 파일 안에서는 허용합니다. 값의 출처가 그 파일 하나입니다 |
| 스크롤바, 폼 컨트롤, 기본 배경 | 브라우저 UI도 따르도록 color-scheme을 선언합니다 |
색과 box-shadow | 테마마다 토큰 값을 정합니다. 한 파일에서만 써도 토큰으로 둡니다 |
| 다크 모드를 지원하지 않기로 함 | prefers-color-scheme을 쓰지 않습니다. 일부 화면에만 적용하지 않습니다 |
어두운 배경에서는 검은 그림자가 보이지 않으므로 그림자도 테마별로 조정합니다. 색과 그림자의 토큰화는 규칙 5.2의 한 파일 예외보다 우선합니다. 토큰 이름은 규칙 5.5를 따릅니다.
컴포넌트 파일에서 테마를 분기합니다.
.pg_products__panel {
background-color: var(--app-color-surface);
@media (prefers-color-scheme: dark) {
background-color: #1f2225;
}
}컴포넌트는 토큰만 씁니다.
.pg_products__panel {
background-color: var(--app-color-surface);
color: var(--app-color-text-primary);
border: 1px solid var(--app-color-border);
box-shadow: var(--app-shadow-panel);
}그림자를 직접 적어 어두운 배경에서 사라집니다.
.pg_products__panel {
box-shadow: 0 1px 3px rgb(0 0 0 / 12%);
}토큰 파일에서 값을 바꾸고 사용자 테마를 시스템 설정보다 우선합니다.
:root {
color-scheme: light;
--app-color-surface: #fff;
--app-color-text-primary: #212529;
--app-color-border: #dee2e6;
--app-shadow-panel: 0 1px 3px rgb(0 0 0 / 12%);
}
@media (prefers-color-scheme: dark) {
:root {
color-scheme: dark;
--app-color-surface: #1f2225;
--app-color-text-primary: #e9ecef;
--app-color-border: #3a3f44;
--app-shadow-panel: 0 1px 3px rgb(0 0 0 / 60%);
}
}
/* [data-theme]는 뒤에 오고 명시도도 높아 위 @media 블록보다 우선 */
:root[data-theme="light"] {
color-scheme: light;
--app-color-surface: #fff;
--app-color-text-primary: #212529;
--app-color-border: #dee2e6;
--app-shadow-panel: 0 1px 3px rgb(0 0 0 / 12%);
}
:root[data-theme="dark"] {
color-scheme: dark;
--app-color-surface: #1f2225;
--app-color-text-primary: #e9ecef;
--app-color-border: #3a3f44;
--app-shadow-panel: 0 1px 3px rgb(0 0 0 / 60%);
}함께 보기
5.5 토큰 이름은 값이 아니라 쓰임으로 짓습니다
값이 바뀌어도 토큰 이름이 쓰임을 나타내고 일관된 형식을 유지합니다.
토큰 이름은 값이 아니라 쓰임을 나타내는 --app-<종류>-<쓰임> 형태로 짓습니다.
값이 바뀌어도 이름이 뜻을 유지해야 하므로 --app-color-white, --app-space-16처럼 짓지 않습니다.
| 종류 | 예 |
|---|---|
color | --app-color-surface, --app-color-text-primary, --app-color-border |
shadow | --app-shadow-panel |
space | --app-space-inline, --app-space-section |
radius | --app-radius-control |
z-index | --app-z-index-sticky. 층 이름은 규칙 5.3를 따릅니다 |
app- 접두사는 규칙 8.1의 custom-property-pattern이 검사합니다.
토큰화 대상은 규칙 5.2,
테마별 값은 규칙 5.4가 정합니다.
값으로 이름을 짓습니다.
:root {
--app-color-white: #fff;
--app-color-gray-100: #f1f3f5;
--app-space-16: 16px;
}
/* src/page/products/pg-products.css */
.pg_products__panel {
background-color: var(--app-color-white);
padding: var(--app-space-16);
}쓰임으로 이름을 짓습니다.
:root {
--app-color-surface: #fff;
--app-color-surface-muted: #f1f3f5;
--app-space-section: 16px;
}
/* src/page/products/pg-products.css */
.pg_products__panel {
background-color: var(--app-color-surface);
padding: var(--app-space-section);
}함께 보기