04 — Line Height & Leading Trim
이 문서가 답하는 질문: 디자인 시안에서 글자 위아래로 정확히 16px 패딩을 줬는데 코드로 옮기면 위가 더 비어 보이는 이유는 무엇인가? 그리고 그걸 어떻게 수학적으로 제거하는가? 한 줄 답 (Pyramid Top):
line-height가 글자 위아래로 만드는 빈 공간(half-leading) 때문이다 — 이걸 해결하는 것이text-box-trim(CSS 표준, Chrome 133+)과 그 폴리필인 Capsize 라이브러리다. 디자인 시스템의 모든 타이포 토큰은font-size + line-height + letter-spacing의 3-튜플로 정의해야 한다.
Why — Half-leading 문제
1) “디자이너 시안 vs 실제 렌더링” 의 간극
디자이너가 Figma에서 “이 버튼은 위아래 12px padding” 이라고 정해도, 코드에 적용하면 위쪽이 더 비어 보임.
font-size: 16px+line-height: 24px→ 8px 의 leading이 발생.- 이게 위 4px + 아래 4px로 나뉘어 분배됨 = half-leading.
- 디자이너의 padding 12px + half-leading 4px = 시각적 padding 16px.
2) Figma는 시각 기준, CSS는 box 기준
| 기준 | Figma | CSS |
|---|---|---|
| Padding 측정 시작점 | 글자의 cap height 또는 baseline | 글자가 든 line-box의 가장자리 |
| half-leading | 자동 제거 (visual) | 그대로 노출 |
3) 모든 타이포는 3-튜플
font-size만 토큰화하고 line-height를 자유에 맡기면 어디서나 different alignment가 발생.
| 풀려는 문제 | 이전의 해법 | 한계 |
|---|---|---|
| 디자인-개발 간극 | ”padding 줄여서 맞춤” | 새 폰트로 바꾸면 다시 깨짐 |
| half-leading 가시 | position: relative; top: -2px | 폰트 메트릭 따라 다름 |
| 표준화 부재 | 폰트마다 manual override | scale 불가 |
How — line-height 토큰화
1) Role 기반 line-height
| 용도 | line-height | 비율 (font-size 16 기준) |
|---|---|---|
display | 1.1 ~ 1.2 | 큰 글자는 tight 해야 시각적 결속 |
heading | 1.2 ~ 1.3 | 적당한 tight |
body | 1.5 ~ 1.6 | 가독성의 default |
caption | 1.4 | 작은 글자는 느슨하게 |
code | 1.5 ~ 1.7 | 모노스페이스는 더 느슨 |
2) 단위 — unitless가 정답
/* ❌ unit 있음 — 상속 시 *계산된 px* 가 상속됨 */
.parent { line-height: 1.5em; font-size: 16px; } /* line-height = 24px */
.child { font-size: 20px; } /* line-height = 여전히 24px! */
/* ✅ unitless — *비율* 이 상속됨 */
.parent { line-height: 1.5; font-size: 16px; } /* line-height = 24px */
.child { font-size: 20px; } /* line-height = 30px ✓ */항상 unitless (
1.5,1.6)로 정의.em,px,%금지.
3) Font-size token과 bundle
// Panda CSS - textStyles로 3-튜플 묶기
textStyles: {
"body.md": {
value: {
fontSize: "1rem", // 16px
lineHeight: "1.5",
letterSpacing: "0",
fontWeight: "400"
}
},
"heading.xl": {
value: {
fontSize: "2.25rem", // 36px
lineHeight: "1.2",
letterSpacing: "-0.02em", // 큰 글자는 *negative tracking*
fontWeight: "700"
}
}
}사용:
<h1 className={css({ textStyle: "heading.xl" })}>Title</h1>What — text-box-trim (모던 CSS 표준)
2024년 표준화된 CSS 속성. half-leading을 완전히 제거.
.tight-text {
text-box-trim: trim-both;
text-box-edge: cap alphabetic;
}속성 설명
| 속성 | 값 | 효과 |
|---|---|---|
text-box-trim | trim-both / trim-start / trim-end / none | 위/아래/양쪽의 half-leading 제거 |
text-box-edge | cap alphabetic / ex alphabetic / text | 어느 메트릭을 가장자리로 볼 것인가 |
text-box-edge 값
| 값 | 위쪽 기준 | 아래쪽 기준 | 용도 |
|---|---|---|---|
text | 폰트의 ascender | descender | default (큰 차이 없음) |
cap alphabetic | cap-height | baseline | UI 표준 (Helvetica I·H 의 윗선↔baseline) |
ex alphabetic | x-height | baseline | 본문에서 시각적 결속 강함 |
브라우저 지원
| 브라우저 | 지원 |
|---|---|
| Chrome 133+ | ✓ |
| Safari 18.2+ | ✓ |
| Firefox | 진행 중 (2026 예정) |
→ 2026년 현재 점진 도입 단계. 폴백 필요.
What — Capsize (폴리필)
Brave/Atlassian이 만든 라이브러리. 폰트 메트릭(ascent, descent, capHeight, xHeight)을 읽고 제거할 padding 값을 자동 계산.
설치
npm install @capsizecss/core @capsizecss/metrics사용
import { createStyleObject } from '@capsizecss/core';
import { interFontMetrics } from '@capsizecss/metrics/inter';
const styles = createStyleObject({
fontSize: 16,
leading: 24, // 원하는 line-height
fontMetrics: interFontMetrics
});
// 결과:
// {
// fontSize: '16px',
// lineHeight: '24px',
// '::before': { content: '""', marginBottom: '-0.3125em', display: 'table' },
// '::after': { content: '""', marginTop: '-0.3125em', display: 'table' }
// }원리
- 폰트 메트릭에서 cap-height ratio 추출 (예: Inter는 0.728).
(ascent + descent - cap-height) / 2= 위쪽 trim 양.::before/::after의 negative margin으로 line-box를 위아래로 잘라냄.
What — text-box-trim + Capsize 하이브리드
2026년 현재 가장 안전한 패턴:
.text-trim {
/* 모던 — Chrome 133+, Safari 18.2+ */
text-box-trim: trim-both;
text-box-edge: cap alphabetic;
}
/* Firefox 등 미지원 — Capsize fallback */
@supports not (text-box-trim: trim-both) {
.text-trim::before {
content: "";
display: table;
margin-bottom: calc(-0.5 * (var(--line-height) - var(--cap-height)));
}
.text-trim::after {
content: "";
display: table;
margin-top: calc(-0.5 * (var(--line-height) - var(--cap-height)));
}
}또는 Capsize의 @capsizecss/unpack를 빌드 타임에 돌려 모든 폰트의 메트릭을 CSS variable로:
@font-face {
font-family: "Inter";
src: url("/fonts/Inter-Variable.woff2") format("woff2-variations");
}
:root {
/* @capsizecss/metrics/inter 에서 추출 */
--font-inter-ascent: 2728;
--font-inter-descent: 680;
--font-inter-cap-height: 2048;
--font-inter-x-height: 1536;
--font-inter-units-per-em: 2816;
}What-if — 잘못 쓰면 어떻게 깨지는가
- 함정 1:
line-height: 1.5em사용 — 자식 요소에서 부모의 px가 그대로 상속되어 다른 폰트 크기에서 비례가 깨짐. 대응: 항상 unitless (1.5). - 함정 2:
font-size만 토큰화,line-height자유 — 같은body.md가 어떤 곳은1.4, 어떤 곳은1.6→ 수직 리듬 깨짐. 대응: textStyles 3-튜플로 묶음. - 함정 3: 전역
text-box-trim: trim-both— 모든 텍스트에 적용하면 body 본문의 가독성 떨어짐 (line-height 충분히 필요한 곳). 대응: button·heading·label 같은 짧은 텍스트에만 적용. - 함정 4: 폰트 교체 후 Capsize metric 업데이트 안 함 — 새 폰트의 cap-height ratio가 다른데 예전 폰트의 trim 값 적용 → 오버 트림. 대응: 폰트 교체 시
@capsizecss/unpack다시 실행. - 함정 5: web font 로드 전 fallback 폰트 — Capsize trim은 Inter용인데 fallback이 Arial이면 cap-height 다름 → FOUT 시 글자 위치가 튐. 대응: fallback 폰트도 같은 cap-height ratio가 되도록
size-adjust로 보정 (Font Adjust API).
Insight — 왜 30년 동안 half-leading을 못 잡았나
line-height는 1996년 CSS1 명세부터 있던 속성이다. 그런데 half-leading 제거는 2024년에야 표준이 됐다 — 거의 30년 만이다.
이유는 역사적 호환성. 90년대 브라우저는 활자 인쇄(letterpress)의 leading 모델을 그대로 가져왔다. 인쇄에서는 글자 위아래로 공기가 있어야 다음 줄과 번지지 않음 → 자연스러운 가독성. CSS도 똑같이 line-box의 위아래에 leading을 분배했다.
하지만 UI 디자인에서는 정반대 — 글자의 시각적 가장자리에 정확히 padding을 정렬하고 싶다. 30년간 디자이너들은 “위쪽 -2px, 아래쪽 -2px” 같은 수동 보정으로 살았다.
2020년경 Atlassian의 디자인 시스템 팀이 Capsize를 발표 — 처음으로 모든 폰트의 cap-height ratio를 JSON으로 배포하고, 그 값으로 자동 trim을 만들어주는 공학적 해법. 이게 컴포넌트 라이브러리(Chakra, Tamagui 등)에 빠르게 퍼졌다.
CSS Working Group은 Capsize의 알고리즘을 표준화한 것이 text-box-trim이다. 디자이너의 30년 묵은 불만을 런타임 비용 0으로 해결한 드문 케이스.
요약
- 모든 텍스트는 line-height로 인한 half-leading을 가짐 — 위아래로 시각적 공기.
- 디자인 시스템의 타이포는
font-size + line-height + letter-spacing3-튜플로 토큰화. line-height는 unitless (1.5,1.6).- 모던:
text-box-trim: trim-both(Chrome 133+, Safari 18.2+). - 폴백: Capsize의 negative margin 트릭.
- button·heading에만 적용. body 본문은 그대로 두는 게 가독성에 좋음.