03 — Inheritance & Value Stages
한 줄 답: 상속(inheritance)은 cascade의 다음 단계다 — cascade가 어떤 값이 선언되었나를 정하면, inheritance는 선언이 없을 때 부모 값을 가져다 쓸지를 정한다. 그리고 그 값은 specified → computed → used → actual의 4단계를 거쳐 화면에 도달한다.
Why — 왜 상속이 필요한가
<article>
<h1>제목</h1>
<p>본문 <em>강조</em></p>
</article>여기서 article { color: navy }만 쓰면 h1, p, em 모두 navy가 된다. 부모에 한 번 쓰면 자식에 자동 적용되는 게 inheritance다.
이게 없다면 모든 요소에 색을 일일이 써야 한다. CSS의 cascade 정신 자체가 이 자동 전파를 전제로 한다 — cascade라는 단어가 “폭포처럼 흘러내림”인 이유.
하지만 모든 속성이 상속되는 건 아니다. border가 상속되면 부모 박스에 border를 그리는 순간 모든 자식에 border가 생긴다 — 재앙. 그래서 명세는 속성별로 기본 상속 여부를 정해뒀다.
How — 상속의 4단계 가치 흐름
1) 값의 4 단계 (CSS Values 4)
브라우저가 한 속성 값을 결정하는 과정:
선언된 값 (declared)
↓ cascade
지정된 값 (specified)
↓ inherit/initial resolution
계산된 값 (computed)
↓ context (font-size, vw 등 해결)
사용된 값 (used)
↓ device clamping (반올림, 픽셀 그리드)
실제 값 (actual)| 단계 | 의미 | 예: width: 50% (부모 800px) |
|---|---|---|
| specified | CSS에 쓴 그대로 | 50% |
| computed | 절대화 가능한 만큼 | 50% (부모를 모를 수 있어 그대로 유지) |
| used | 레이아웃 후 실제 픽셀 | 400px |
| actual | 장치에 그릴 픽셀 | 400px (서브픽셀 반올림) |
| 단계 | 예: font-size: 1.5em (부모 16px) |
|---|---|
| specified | 1.5em |
| computed | 24px (font-size는 computed에서 즉시 절대화) |
| used | 24px |
| actual | 24px |
중요: 상속되는 것은 computed value다. 자식이 부모의 font-size: 1.5em을 상속받으면, 1.5em이 아니라 부모의 computed 24px를 받는다 — 그래서 손자가 또 1.5em이면 36px이 된다(누적).
2) 상속되는 속성 vs 상속되지 않는 속성
기본 상속 ✓:
- 텍스트 관련:
color,font-*,line-height,letter-spacing,text-align,text-indent,text-transform,white-space,word-spacing,direction - 리스트:
list-style - 테이블:
border-collapse,border-spacing,caption-side - 음성:
voice-*,cursor - 가시성:
visibility(단,display는 아님) - 모든 custom property (
--*)
기본 상속 ✗:
- 박스 모델:
margin,padding,border,width,height - 배경:
background-* - 위치:
position,top/right/bottom/left,z-index - display, float, clear
- transform, opacity, filter
- animation, transition
3) 강제 상속/초기화 키워드 (CSS-wide values)
모든 속성에 쓸 수 있는 4(+1)개의 키워드:
| 키워드 | 동작 |
|---|---|
initial | 명세의 initial value로 (예: color는 canvastext) |
inherit | 부모의 computed value를 강제로 가져옴 (상속 안 되는 속성도 가능) |
unset | 상속되는 속성은 inherit, 아니면 initial |
revert | 이 origin을 무시 → 더 약한 origin(UA 기본)로 |
revert-layer | 이 layer를 무시 → 더 약한 layer로 |
button {
all: unset; /* 모든 속성 초기화 */
font: inherit; /* 폰트만 부모와 동일 */
cursor: pointer;
}all: unset은 버튼/링크 reset의 표준 패턴이다.
revert-layer는 @layer 시대의 핵심. layer에서 적용한 스타일을 그 layer 안에서만 무효화해 더 낮은 layer의 값으로 돌릴 수 있다.
What — 자주 보는 케이스
케이스 1: color는 상속, background-color는 안 됨
body { color: navy; background-color: #fafafa; }자식 텍스트는 모두 navy(상속). 하지만 자식의 배경은 각자 transparent(기본값). 그래서 body에 회색을 깔아도 자식 div들은 자기 배경을 갖지 않는다 — 그런데 시각적으로 부모 회색이 비쳐 보일 뿐.
이건 상속이 아니라 transparent 기본값이라는 걸 헷갈리면 안 된다.
케이스 2: font-size 누적
.outer { font-size: 1.5em; }
.middle { font-size: 1.5em; }
.inner { font-size: 1.5em; }html { font-size: 16px } 가정 시:
.outer→ 24px.middle→ 36px (24 × 1.5).inner→ 54px (36 × 1.5)
em은 상속된 부모의 computed font-size에 곱해진다. 누적을 피하려면 rem(root em — html의 font-size 기준).
케이스 3: custom property는 항상 상속
:root { --brand: oklch(60% 0.2 250); }
button { background: var(--brand); }
.dark { --brand: oklch(40% 0.2 250); } /* 이 안에서 redefine */custom property는 항상 상속된다. 그래서 다크모드 전환의 핵심 도구.
.dark button {
background: var(--brand); /* 자동으로 .dark의 --brand 사용 */
}케이스 4: inherit으로 상속 안 되는 속성 강제 전달
.card {
border: 1px solid currentColor;
}
.card * {
border-color: inherit; /* border는 상속 안 되지만, color는 됨 */
}또는:
.dialog { background: white; }
.dialog * { background: inherit; } /* 자식 모두 강제로 white 배경 */(실제로는 거의 안 쓴다 — 위험. 하지만 표현은 가능.)
케이스 5: revert-layer로 layer만 풀기
@layer base, theme;
@layer base {
button { padding: 8px 16px; background: gray; }
}
@layer theme {
button { background: blue; }
.reset-theme {
background: revert-layer; /* base의 gray로 돌아감 */
}
}케이스 6: animation 중의 inheritance
.parent { color: red; }
.parent { animation: c 1s; }
@keyframes c {
to { color: blue; }
}
.child {
/* 부모의 *animation-active* color를 상속 */
/* 초당 red → blue로 변하는 동안 child도 같이 변한다 */
}자식은 부모의 현재 computed color를 상속한다 — animation 중에는 그 값이 매 프레임 바뀐다.
What-if — 자주 깨지는 패턴
사고 1: form 안의 폰트가 안 따라옴
<body style="font-family: 'Pretendard'">
<input type="text">
</body>input은 폰트가 상속되지 않는다 — Form 컨트롤은 UA가 자기 폰트(-webkit-small-control 등)를 강제로 박는다. 해결:
input, button, select, textarea {
font: inherit;
}이게 normalize.css의 단골 항목인 이유.
사고 2: 다크모드 전환 시 일부 색만 바뀜
:root { --text: black; }
.dark { --text: white; }
h1 { color: black; } /* 하드코딩 — 안 바뀜 */
p { color: var(--text); } /* 토큰 — 바뀜 */color: var(--text)로 일관되게 쓰지 않으면 다크모드가 깨진다. 변수가 상속되어 자동 전파되는 걸 활용해야 토큰 전략이 성립.
사고 3: line-height: 1.5em vs line-height: 1.5
body { font-size: 16px; line-height: 1.5em; } /* computed: 24px — 그대로 상속 */
h1 { font-size: 32px; } /* line-height는 여전히 24px → 좁음! */body { line-height: 1.5; } /* unitless — 매번 자기 font-size × 1.5 */
h1 { font-size: 32px; } /* line-height = 48px — 정상 */unitless line-height만 써라. 단위가 붙으면 computed value로 절대화되어 상속 시 자식의 font-size 변화를 못 따라온다.
사고 4: width: inherit의 함정
.parent { width: 50%; }
.child { width: inherit; } /* "50%" 그대로 — 부모 width의 50%가 아님 */width는 상속되지 않는 속성이라 inherit을 명시해야 하는데, computed value가 상속된다 — 즉 50%라는 문자가 그대로 자식에 박힌다. 자식은 *자기 부모(=.parent)의 50%*가 되므로 결과적으로 손주의 50%가 .parent의 50%의 50%가 아니라 .parent의 50%. 헷갈린다. 그래서 거의 안 쓴다.
Insight — revert 키워드와 캐스케이드의 반전
revert(2020)와 revert-layer(2022)는 cascade에 역방향 화살표를 추가한 사건이다.
전통적으로 cascade는 위에서 아래로 — 더 강한 규칙이 약한 규칙을 덮는 단방향이었다. 그래서 “라이브러리가 박은 스타일을 풀고 UA 기본으로 돌아가고 싶다”는 덮어쓰기로만 가능했다. 즉, 적절한 기본값을 찾아 다시 쓰는 수밖에.
/* 옛 방식 — UA 기본을 모르면 못 씀 */
button { padding: 2px 6px; background: buttonface; ... }revert는 이걸 **“이 origin을 그냥 무시해줘”**라는 선언적 풀기로 바꿨다.
/* 모던 */
button { all: revert; } /* UA 기본으로 깨끗하게 */이는 cascade를 *“앞으로 쌓는 스택”*에서 *“각 layer가 켜고 끌 수 있는 OFF 스위치 가진 스택”*으로 진화시켰다. @layer + revert-layer의 조합은 디자인 시스템에서 **“이 컴포넌트는 디자인 토큰을 안 받겠다”**를 한 줄로 표현 가능하게 한다.
요약 + Mermaid
- 상속은 cascade의 다음 단계다. cascade가 선언된 값을 정하면 inheritance가 없을 때 부모 값을 가져온다.
- 값은 4단계: specified → computed → used → actual.
- 상속되는 것은 computed value.
- 텍스트 관련은 대부분 상속, 박스/배경/위치는 안 됨. custom property는 항상 상속.
- 5개 키워드:
initial,inherit,unset,revert,revert-layer.all: unset은 reset 핵심.
다음: 04-cascade-layers에서 @layer를 완전 정복한다.