Skip to content
This page has been auto-translated and may contain errors.View in English

커스텀 속성과 모던 CSS

docs.scrimba.com

브랜드 블루를 하나 정하고, 마흔 개의 규칙에 그 색을 일일이 입력하고, 사이트를 배포합니다. 한 달 뒤 마케팅팀에서 블루 톤을 조금 바꿔달라고 합니다. 이제 모든 스타일시트를 뒤지면서 마흔 곳을 전부 찾았는지, 대문자로 적어놓은 두 곳을 빠뜨리지 않았는지 노심초사해야 합니다. 커스텀 속성은 이 문제를 해결해줍니다. 색상 값에 이름을 한 번 붙이고, 그 이름을 어디서나 쓰고, 필요할 때 한 곳만 바꾸면 됩니다. 이 작은 습관 하나가 테마 적용, 디자인 토큰, 그리고 리디자인에도 살아남는 스타일시트의 출발점이 됩니다.

변수 선언하고 사용하기

**커스텀 속성(custom property)**은 재사용할 수 있도록 이름을 붙인 값입니다. 이름 앞에 대시(-) 두 개를 붙여서 값을 지정하고, 나중에 var()로 그 값을 다시 읽어옵니다.

css
:root {
  --color-brand: #2563eb;   /* 값을 이름 아래 한 번만 선언 */
}

.button {
  background: var(--color-brand);   /* 여기서 읽어옴 */
}

.link {
  color: var(--color-brand);        /* 여기서도, 같은 값 */
}

--color-brand가 이름이고(대시 두 개는 필수입니다), var(--color-brand)가 그 값을 사용하는 방법입니다. :root(페이지 최상단에 있는 <html> 요소)에 선언하면 이 이름을 어디서나 쓸 수 있게 됩니다. :root에 있는 한 줄만 바꾸면 이 값을 읽는 모든 규칙이 함께 바뀝니다.

**커스텀 속성(custom property)**은 -- 접두사를 붙여 선언하고 var() 함수로 다시 읽어오는, 이름이 붙은 값입니다. CSS 변수라고도 부르는데, 두 명칭은 같은 의미입니다.

css
:root {
  --color-brand: #2563eb;
  --color-brand-dark: #1e40af;
}

.button {
  background: var(--color-brand);
}

.button:hover {
  background: var(--color-brand-dark);
}

-- 접두사는 커스텀 속성을 일반 속성과 구분해주는 표시이며, 대소문자를 구분하므로 --color-Brand--color-brand는 서로 다른 이름입니다. :root에 선언하는 것이 일반적인데, 트리 최상단에 값을 두면 모든 요소가 이 값을 볼 수 있고, 전역 토큰이 있어야 할 자리가 바로 여기이기 때문입니다. 색상에만 쓸 수 있는 것은 아니며, 어떤 속성이든 받아들이는 값이면 길이든, 그림자든, 폰트 스택이든 커스텀 속성에 담을 수 있습니다.

**커스텀 속성(custom property)**은 이름이 --로 시작하는 사용자 정의 속성으로, var() 함수를 통해 값을 다시 읽어옵니다. 이름은 대소문자를 구분하며, 값은 거의 그대로 저장됩니다. 선언 시점에 브라우저가 값을 손대는 일은 거의 없는데, 이 값이 실제로 어디서 해석(resolve)되는지를 이해하면 이 특성이 중요해집니다.

css
:root {
  --color-brand: #2563eb;
  --space-md: 16px;
  --shadow-card: 0 1px 3px rgb(0 0 0 / 0.12);
}

.card {
  padding: var(--space-md);
  box-shadow: var(--shadow-card);
}

:root에 선언하는 것은 관례일 뿐 필수는 아닙니다. :root<html> 요소와 매칭되며 html 타입 셀렉터보다 명시도가 약간 더 높고, 커스텀 속성은 상속되므로 여기서 설정한 값은 모든 자손 요소에 보이게 됩니다. 이렇게 이해하는 것이 맞는 사고방식입니다. 전역 레지스트리를 만드는 것이 아니라, 루트 요소에 속성을 하나 설정하는 것이고, 그 속성이 문서 전체로 상속되어 내려가는 것뿐입니다. 일반 속성이 받아들이는 값이라면 무엇이든 저장할 수 있고, 색상 채널이나 단위 없는 숫자 같은 값의 일부만 저장해서 나중에 조합하는 경우도 있습니다.

Juno변수 선언하고 사용하기 커스텀 속성은 이름이 붙은 값입니다. --color-brand처럼 대시 두 개로 적고, var(--color-brand)로 읽어옵니다. :root에 두면 페이지 전체에서 쓸 수 있습니다. 한 줄만 바꾸면 그 이름을 읽는 모든 규칙이 함께 바뀝니다.
Juno변수 선언하고 사용하기-- 접두사가 커스텀 속성을 나타내고, var()가 값을 읽어옵니다. :root에 선언하면 전역 토큰이 트리 최상단에 자리 잡아 모든 요소가 접근할 수 있습니다. 이름은 대소문자를 구분하고, 색상뿐 아니라 속성이 받아들이는 어떤 값이든 담을 수 있습니다.
Juno변수 선언하고 사용하기 커스텀 속성은 --name 형태로 설정하고 var()로 읽어오는 값으로, 해석되기 전까지는 거의 그대로 저장됩니다. :root는 관례일 뿐이며 실제로는 <html> 요소이고, 이 속성들이 상속되기 때문에 여기서 설정한 값은 모든 자손에 전달됩니다. 값의 일부만 저장해서 나중에 조합할 수도 있는데, 폴백 섹션에서 이 방식을 다루기 시작합니다.

캐스케이드와 상속

다른 언어에서 변수를 써본 적이 있다면 다음 부분이 뜻밖일 수 있습니다. 커스텀 속성은 고정된 값이 아닙니다. 다른 CSS 속성과 똑같은 규칙을 따르기 때문에 자식 요소로 **상속**되며, 나머지는 그대로 두고 한 곳에서만 값을 다르게 줄 수 있습니다.

css
:root {
  --color-text: #1f2937;   /* 페이지 전체의 기본값 */
}

.callout {
  --color-text: #92400e;   /* .callout 안에서만 다른 값 */
  color: var(--color-text);
}

p {
  color: var(--color-text);   /* 보통은 짙은 회색, callout 안에서는 호박색 */
}

같은 var(--color-text)인데 대부분의 문단에서는 짙은 회색이고 callout 안에서는 호박색이 되는 것은, callout이 자기만의 값을 설정했기 때문입니다. 이것이 테마 적용을 가능하게 해주는 원리이며, 그 뒤에 있는 캐스케이드는 CSS의 동작 원리에서 이미 살펴봤습니다.

바로 이 지점에서 커스텀 속성은 Sass 같은 CSS 전처리기의 변수와 갈라집니다. 전처리기 변수는 브라우저가 보기 전에 컴파일 과정에서 사라지므로, 정적인 찾아 바꾸기에 불과합니다. 커스텀 속성은 브라우저 안에 실제로 살아있는 진짜 CSS 속성이므로 캐스케이드를 따르고 **상속**됩니다.

커스텀 속성의 값은 선언된 위치가 아니라, 그 값을 읽는 요소를 기준으로 찾습니다. 바로 이 한 가지 사실 때문에 컴포넌트 테마 적용이 가능해집니다.

css
:root {
  --color-accent: #2563eb;   /* 기본값은 어디서나 blue */
}

.card--warning {
  --color-accent: #d97706;   /* 이 컴포넌트 서브트리에서는 override */
}

.card__badge {
  background: var(--color-accent);   /* blue이거나, warning 카드 안이면 amber */
}

badge는 --color-accent가 어디서 왔는지 알지도 못하고 신경 쓰지도 않습니다. 자기 자신에게 적용된 값을 읽을 뿐이고, 조상 요소 중 누구든 그 값을 재정의할 수 있습니다. 컨테이너에서 토큰을 override하면 그것을 읽는 모든 자손이 컴포넌트 규칙을 한 줄도 건드리지 않고 스킨이 바뀝니다. 캐스케이드와 상속의 전체 규칙은 CSS의 동작 원리에 있습니다.

커스텀 속성을 전처리기 변수와 구분 짓는 특징은, 기본적으로 캐스케이드에 참여하고 **상속**된다는 점입니다. Sass나 Less의 변수는 빌드 시점에 해석되고 브라우저가 파일을 로드하기 전에 사라집니다. 커스텀 속성은 요소에 실제로 존재하는 속성으로, colorfont-size를 지배하는 것과 동일한 캐스케이드에 의해 해석됩니다. 즉 var()그것을 읽는 요소에서 계산된 속성값을 기준으로 찾습니다.

css
:root {
  --color-accent: #2563eb;
}

.panel[data-tone="warning"] {
  --color-accent: #d97706;   /* 서브트리의 루트에 설정 */
}

.panel__button {
  background: var(--color-accent);   /* 요소별로 해석되며, panel로부터 상속 */
}

여기서 기억해둘 결과가 두 가지 있습니다. 첫째, 상속이 테마 적용을 해냅니다. 서브트리 루트에 --color-accent를 설정하면 그것을 읽는 모든 자손으로 전파되므로, 컨테이너에서의 override 하나로 규칙을 바꾸지 않고도 컴포넌트 트리 전체의 스킨을 바꿀 수 있습니다. 둘째, 명시도는 여전히 토큰이 설정되는 위치에 적용되며, var() 자체에 적용되는 것이 아닙니다. 두 셀렉터가 같은 요소에 --color-accent를 설정한다면, 다른 속성과 마찬가지로 일반적인 캐스케이드 규칙이 어느 쪽이 이길지 결정합니다. 상속을 멈춰야 하는 속성은 initial로 리셋할 수도 있고, @property로 상속되지 않도록 선언할 수도 있는데, 이는 아래 심화 학습에서 다룹니다. 캐스케이드 규칙은 예외 없이 여기에도 적용됩니다.

Juno캐스케이드와 상속 커스텀 속성은 고정된 값이 아닙니다. 자식 요소로 상속되며, 나머지는 그대로 두고 한 곳에서만 다른 값을 설정할 수 있습니다. 그래서 같은 var(--color-text)가 대부분의 문단에서는 회색이고 callout 안에서는 호박색이 될 수 있습니다. 컨테이너에서 그 이름을 다시 정의하면 그 안의 모든 것이 새 값을 따라갑니다.
Juno캐스케이드와 상속 빌드 시점에 사라지는 Sass 변수와 달리, 커스텀 속성은 브라우저 안에서 실제로 살아있고 캐스케이드를 따르므로, 값은 그것을 사용하는 요소를 기준으로 읽힙니다. 컨테이너에서 토큰을 override하면 컴포넌트 규칙을 하나도 건드리지 않고 모든 자손의 스킨이 바뀝니다. 읽는 자리에서 값을 찾는 이 방식이 테마 적용의 핵심 트릭입니다.
Juno캐스케이드와 상속 커스텀 속성은 상속되고 캐스케이드되지만, 전처리기 변수는 브라우저가 로드하기 전에 이미 사라지므로 그렇지 않습니다. var()는 읽는 요소에서 계산된 값을 기준으로 해석되므로, 서브트리 루트에 토큰을 설정하면 상속만으로 그 아래 전체에 테마가 적용됩니다. 명시도는 토큰이 설정되는 위치를 지배할 뿐, var() 호출 자체를 지배하지는 않습니다.

폴백과 calc()

var()는 두 번째 값을 받을 수 있는데, 이는 커스텀 속성이 설정되지 않았을 때만 사용됩니다. 이것이 **폴백(fallback)**이고, 토큰이 없을 때도 규칙이 계속 작동하게 해줍니다.

css
.card {
  gap: var(--gap, 1rem);   /* --gap이 있으면 사용, 없으면 1rem */
}

calc()를 이용하면 커스텀 속성으로 계산도 할 수 있습니다. 기본 여백 값이 하나 있다면, 두 번째 숫자를 하드코딩하는 대신 그 값을 바탕으로 더 큰 값을 만들 수 있습니다.

css
:root {
  --space-md: 16px;
}

.section {
  padding: calc(var(--space-md) * 2);   /* 32px, 기본값에서 파생 */
}

이렇게 값을 파생시키면 --space-md 하나를 바꿀 때 그 위에 만들어진 모든 값이 함께 바뀝니다.

var()폴백(fallback) 역할을 하는 두 번째 인자를 받을 수 있습니다. var(--gap, 1rem)--gap이 설정되어 있으면 그 값으로, 설정되지 않았으면 1rem으로 해석됩니다. 이 폴백은 특정 서브트리에 정의되어 있지 않을 수 있는 토큰이라면 어디에든 써두는 것이 좋습니다. 그러면 토큰이 제공되지 않은 곳에 컴포넌트가 놓여도 여전히 합리적으로 렌더링됩니다.

css
.card {
  gap: var(--gap, 1rem);
  padding: var(--space-md, 16px);
}

커스텀 속성이 진짜 힘을 발휘하는 곳은 calc()와 함께 쓸 때입니다. 마법의 숫자를 잔뜩 저장해두는 대신, 하나의 값에서 다른 값을 파생시킬 수 있기 때문입니다.

css
:root {
  --space-md: 16px;
}

.section {
  padding: calc(var(--space-md) * 2);        /* 32px */
  margin-bottom: calc(var(--space-md) * 3);  /* 48px, 단일 진실 공급원 */
}

한 가지 주의할 점이 있습니다. 폴백은 쉼표를 포함한 완전한 값이 될 수도 있습니다. 그래서 var(--shadow, 0 1px 2px black)도 유효하며, 첫 번째 쉼표 뒤의 모든 것이 폴백으로 취급됩니다. calc() 안의 단위가 어떻게 해석되는지는 단위 문서를 참고하세요.

var()는 두 번째 인자로 선택적인 폴백을 받습니다. var(--gap, 1rem)은 읽는 요소에서 --gap이 설정되지 않았거나(또는 유효하지 않음이 보장된 값으로 설정되었을 때) 1rem을 반환합니다. **폴백(fallback)**은 첫 번째 쉼표 뒤의 모든 것을 통째로 가져가므로, 폴백 자체가 쉼표를 포함한 값이거나 중첩된 var()가 될 수 있고, 이를 이용하면 기본값을 체인처럼 연결할 수 있습니다: var(--gap, var(--space-md, 1rem)).

css
.card {
  gap: var(--gap, var(--space-md, 1rem));   /* 체인으로 연결된 폴백 */
}

:root {
  --space-md: 16px;
}

.section {
  padding: calc(var(--space-md) * 2);   /* 32px, 하드코딩이 아니라 파생값 */
}

calc()와 짝을 이룰 때 커스텀 속성은 스케일링 시스템에서 진가를 발휘하며, 여기에는 정밀도에 관한 규칙이 하나 따라옵니다. 커스텀 속성은 단위 없는 숫자를 담을 수 있고, calc() 안에서 그 숫자에 단위를 곱해줄 수 있습니다.

css
:root {
  --scale: 1.5;   /* 단위 없음, 그래야 어떤 단위든 곱할 수 있음 */
}

.title {
  font-size: calc(1rem * var(--scale));   /* 1.5rem */
  line-height: calc(var(--scale) + 0.2);  /* 순수한 숫자 연산 */
}

--scale을 단위 없이 저장하는 이유는 calc(var(--scale) * 16px)가 정상적으로 동작하기 때문입니다. 반대로 --scale: 1.5rem으로 저장하고 calc(var(--scale) * 16px)를 시도하면 길이 두 개를 곱하는 셈이 되어 유효하지 않습니다. 원시 비율은 단위 없이 보관하고, 실제로 사용하는 지점에서 단위를 붙이세요. 이 단위들이 픽셀로 어떻게 해석되는지는 단위 문서에서 다룹니다.

Juno폴백과 calc()var(--gap, 1rem)은 토큰이 있으면 그것을 쓰고 없으면 1rem으로 대체하므로, 값이 빠졌을 때도 규칙이 계속 작동합니다. 그리고 calc()를 이용하면 calc(var(--space-md) * 2)처럼 토큰에서 값을 만들어낼 수 있고, 기본 여백의 두 배를 얻을 수 있습니다. 기본값을 바꾸면 그 위에 세워진 모든 값이 함께 따라갑니다.
Juno폴백과 calc()var()의 두 번째 인자는 폴백입니다. var(--gap, 1rem)은 토큰이 설정되지 않은 경우까지 대응해줍니다. 마법의 숫자를 하드코딩하는 대신 커스텀 속성과 calc()를 짝지어 값을 파생시키면, 토큰 하나가 스케일 전체를 이끌게 됩니다. 폴백이 첫 번째 쉼표 뒤의 모든 것을 통째로 가져간다는 점도 기억해두세요. 여러 부분으로 이루어진 값에 유용합니다.
Juno폴백과 calc()var(--gap, 1rem)은 값을 통째로 가져가는 방식으로 폴백을 처리하므로, var(--gap, var(--space-md, 1rem))처럼 기본값을 중첩할 수 있습니다. 원시 비율은 단위 없이 저장하세요. calc(var(--scale) * 16px)는 동작하지만 길이 두 개를 곱하는 것은 동작하지 않기 때문입니다. 단위는 사용하는 지점에서 붙이세요. 그러면 토큰 하나가 타이포그래피나 여백 스케일 전체를 이끌 수 있습니다.

테마 적용

커스텀 속성은 트리 아래쪽에서 다시 정의할 수 있으므로, 클래스 아래에서 토큰만 바꾸는 방식으로 다크 테마를 만들 수 있고, 컴포넌트 규칙은 하나도 건드리지 않아도 됩니다.

css
:root {
  --color-bg: #ffffff;
  --color-text: #1f2937;
}

.theme-dark {
  --color-bg: #0f172a;      /* 이름은 같고, 값만 새로움 */
  --color-text: #e2e8f0;
}

body {
  background: var(--color-bg);
  color: var(--color-text);
}

페이지에 class="theme-dark"를 추가하면 --color-bg--color-text를 읽는 모든 규칙이 다크 값으로 바뀝니다. 테마를 전환하기 위해 body 규칙이나 그 어떤 컴포넌트도 건드리지 않았습니다. 이것이 헥스 코드를 직접 쓰는 대신 색상에 토큰으로 이름을 붙이는 것에 대한 보상입니다.

테마 적용은 캐스케이드되는 토큰이 주는 직접적인 보상입니다. 색상을 토큰으로 한 번 정의하고, 모든 컴포넌트를 그 토큰 이름으로 작성해두면, 테마는 클래스나 미디어 쿼리 아래에서 같은 이름들에 대한 값을 하나 더 마련하는 것에 불과해집니다.

css
:root {
  --color-bg: #ffffff;
  --color-text: #1f2937;
  --color-surface: #f8fafc;
}

.theme-dark {
  --color-bg: #0f172a;
  --color-text: #e2e8f0;
  --color-surface: #1e293b;
}

컨테이너에 theme-dark를 토글하면 그 안의 모든 것이 스킨을 갈아입습니다. 컴포넌트가 고정된 색이 아니라 토큰 이름을 읽기 때문입니다. 같은 방식이 미디어 쿼리에도 적용되므로, 테마가 운영체제 설정을 따라가게 만들 수도 있습니다.

css
@media (prefers-color-scheme: dark) {
  :root {
    --color-bg: #0f172a;
    --color-text: #e2e8f0;
    --color-surface: #1e293b;
  }
}

이 구조가 유지되게 만드는 원칙은 원시 헥스 코드를 컴포넌트 규칙에서 완전히 배제하고, 대신 색상 토큰으로 이름을 붙이는 것입니다. 토큰 명명 규칙이 진가를 발휘하는 지점도 바로 여기인데, CSS 정리하기에서 자세히 다룹니다.

테마 적용이야말로 커스텀 속성이 단순한 값 재사용을 넘어 중요해지는 이유입니다. 테마란 결국 토큰 레이어에 대한 캐스케이드 override일 뿐이기 때문에 작동합니다. 컴포넌트는 토큰 이름을 읽고, 테마는 트리의 더 높은 지점에서 그 이름들을 다시 정의합니다. 컴포넌트 레이어의 어떤 것도 테마가 존재한다는 사실을 알지 못합니다.

css
:root {
  --color-bg: #ffffff;
  --color-text: #1f2937;
  --color-surface: #f8fafc;
}

.theme-dark {
  --color-bg: #0f172a;
  --color-text: #e2e8f0;
  --color-surface: #1e293b;
}

@media (prefers-color-scheme: dark) {
  :root:not(.theme-light) {
    --color-bg: #0f172a;
    --color-text: #e2e8f0;
    --color-surface: #1e293b;
  }
}

실제 프로덕션에서 잘 유지되는 패턴은 2단계 토큰 시스템입니다. 첫 번째 계층은 원시 값을 담고(--blue-600: #2563eb), 두 번째 계층은 그것을 참조하는 시맨틱 토큰을 담습니다(--color-accent: var(--blue-600)). 컴포넌트는 시맨틱 계층만 읽으므로, 테마는 시맨틱 토큰을 다시 할당할 뿐이고 팔레트 계층은 그대로 유지되어, 전환 폭을 작게 유지하면서 의도도 읽기 쉽게 남습니다. 위처럼 클래스 override와 prefers-color-scheme 미디어 쿼리를 함께 쓰면, 컴포넌트 규칙을 중복하지 않고도 시스템 기본값과 수동 토글을 동시에 갖출 수 있습니다. 이를 유지보수 가능하게 만드는 명명 원칙은 나머지 파일 및 토큰 구성과 함께 다루고, 원시 값 자체는 색상에서 다룹니다.

심화 관점에서 커스텀 속성을 볼 때 짚어야 할 것이 세 가지 더 있습니다. 이 셋은 단순히 다듬는 정도가 아니라, 가능한 것의 범위 자체를 바꿔놓습니다.

커스텀 속성은 **computed-value time**에 해석됩니다. 즉 브라우저는 파일을 파싱하는 시점이 아니라 스타일을 계산하는 시점에, 요소별로 var()를 해석합니다. 이것이 바로 테마 적용이 작동하는 이유이자 전처리기 변수는 절대 해낼 수 없는 이유입니다. 값이 살아있기 때문에 캐스케이드, 상속, 그리고 런타임에 일어나는 변경 모두에 반응합니다.

런타임이 두 번째 요점입니다. 이것들은 실제 속성이기 때문에 JavaScript가 읽고 쓸 수 있고, 그 토큰을 읽는 모든 규칙이 즉시 업데이트됩니다.

css
:root {
  --sidebar-width: 280px;   /* 런타임에 JS로 읽고 설정 */
}

스크립트에서 document.documentElement.style.setProperty('--sidebar-width', '320px')를 호출하면 레이아웃이 새 값에 맞춰 다시 흐릅니다. 페이지가 로드되기 훨씬 전에 컴파일되어 사라지는 Sass 변수는 절대로 이런 일을 할 수 없습니다. 이 속성을 루트가 아니라 다른 요소에 설정해서 서브트리 범위로 한정할 수도 있는데, 그러면 변경 사항이 그 트리 가지에만 국한됩니다.

세 번째는 속성 자체에 대한 제어권입니다. @property를 쓰면 커스텀 속성을 타입, 초기값, 상속 여부와 함께 등록할 수 있고, 이를 통해 커스텀 속성을 부드럽게 애니메이션할 수 있게 됩니다(일반 커스텀 속성은 타입이 지정되지 않아 브라우저가 값을 보간할 수 없으므로 켜짐/꺼짐처럼 뚝뚝 끊기며 애니메이션됩니다). 함께 쓰면 좋은 것으로 모던 값 함수들이 토큰과 자연스럽게 어울립니다. clamp(min, preferred, max)는 preferred 값을 우선하되 범위를 벗어나지 않게 해주고, min()은 인자 중 가장 작은 값을, max()는 가장 큰 값을 반환합니다. 그래서 width: min(100%, var(--max-width))는 미디어 쿼리 없이도 유동적인 너비를 토큰 값 이하로 제한할 수 있습니다.

Juno테마 적용 테마란 같은 토큰 이름에 대해 .theme-dark 같은 클래스 아래에서 다른 값을 쓰는 것뿐입니다. class="theme-dark"를 설정하면 그 토큰을 읽는 모든 규칙이 컴포넌트를 하나도 건드리지 않고 전환됩니다. 그래서 각 규칙에 헥스 코드를 직접 타이핑하는 대신 색상에 토큰으로 이름을 붙이는 것입니다.
Juno테마 적용 토큰이 캐스케이드되기 때문에, 테마는 클래스나 prefers-color-scheme 미디어 쿼리 아래에서 같은 이름들에 대한 두 번째 값 집합일 뿐입니다. 원시 헥스 코드를 컴포넌트 규칙에서 배제하고 대신 토큰 이름을 읽게 하면, override 한 번으로 서브트리 전체의 스킨이 바뀝니다. 클래스를 토글하거나 OS 설정에 맞추면 그 토큰을 읽는 모든 것이 따라옵니다.
Juno테마 적용 커스텀 속성은 computed-value time에 해석되므로 살아있는 값입니다. JavaScript가 런타임에 --sidebar-width를 설정할 수 있고 그것을 읽는 모든 규칙이 다시 흐르는데, 전처리기 변수는 이런 일을 할 수 없습니다. 원시 팔레트 토큰과 시맨틱 토�니 2단계로 나누는 구조는 테마 전환을 몇 개의 재할당으로 줄여줍니다. 그리고 @propertyclamp(), min(), max()는 미디어 쿼리 없이도 이 토큰들에 애니메이션을 적용하고 범위를 제한할 수 있게 해줍니다.