Navigation

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가 됩니다

공통 토큰에 대체값을 붙여 값을 두 곳에 둡니다.

src/page/orders/_pg-order-filter-dialog.csscss
.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.csscss
.pg_products__row {
	background: #f5f5f5;
}

/* pg-product-detail.css */
.pg_productDetail__row {
	background: #f5f5f5;
}

여러 파일이 쓰는 값은 전역 공통 토큰으로 둡니다.

src/style/token.csscss
: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-base0일반 흐름
--app-z-index-sticky100sticky 헤더, 툴바
--app-z-index-overlay200모달, 드로어, 백드롭
--app-z-index-popper300툴팁, 드롭다운, 알림

새 용도가 네 층에 모두 맞지 않을 때만 토큰 파일에 층을 추가하고 100 간격을 유지합니다.

쌓임 맥락

층 순서는 같은 쌓임 맥락 안에서만 성립합니다. 조상의 맥락이 바깥 sticky보다 아래면 내부 popper의 숫자를 올려도 그 위로 나오지 못합니다.

새 쌓임 맥락을 만드는 대표 속성조건
positionrelative 또는 absolute이면서 z-index가 auto가 아님
transform, filter, backdrop-filternone이 아님
will-change쌓임 맥락을 만드는 속성을 지정함
opacity, isolation, containopacity는 1 미만, isolation은 isolate, contain은 layout, paint, content, strict 중 하나

fixed와 sticky는 그 자체로 새 쌓임 맥락을 만듭니다.

가려졌을 때 확인 순서

요소가 가려졌으면 숫자를 올리기 전에 아래 순서로 확인합니다.

  1. z-index가 적용되는지 봅니다. 일반 요소는 static이면 적용되지 않고 relative부터 적용됩니다.
  2. flex와 grid 아이템인지 봅니다. static이어도 z-index에 auto가 아닌 값을 주면 적용되고 쌓임 맥락도 만듭니다.
  3. 같은 층 안에서 순서가 충돌하면 층 분류를 다시 봅니다. 값을 +1 하지 않습니다.
  4. 조상의 쌓임 맥락이나 잘림이 원인이면 해당 조상 밖의 포털 대상으로 옮깁니다. 포털도 실제 부착 위치의 DOM 맥락을 따르므로 대상 위치를 확인합니다.
  5. showModal()로 연 dialog나 열린 popover는 최상위 레이어입니다. 일반 문서의 z-index 토큰으로 그 위에 올라가려 하지 않습니다.

숫자를 직접 쓰고 다른 요소보다 하나씩 올립니다.

src/page/products/pg-products.csscss
.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;
}

층 토큰만 씁니다.

src/style/token.csscss
: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를 따릅니다.

컴포넌트 파일에서 테마를 분기합니다.

src/page/products/pg-products.csscss
.pg_products__panel {
	background-color: var(--app-color-surface);

	@media (prefers-color-scheme: dark) {
		background-color: #1f2225;
	}
}

컴포넌트는 토큰만 씁니다.

src/page/products/pg-products.csscss
.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);
}

그림자를 직접 적어 어두운 배경에서 사라집니다.

src/page/products/pg-products.csscss
.pg_products__panel {
	box-shadow: 0 1px 3px rgb(0 0 0 / 12%);
}

토큰 파일에서 값을 바꾸고 사용자 테마를 시스템 설정보다 우선합니다.

src/style/token.csscss
: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가 정합니다.

값으로 이름을 짓습니다.

src/style/token.csscss
: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);
}

쓰임으로 이름을 짓습니다.

src/style/token.csscss
: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);
}

함께 보기