🧩 Design System3. Typography & Spacing04 — Line Height & Leading Trim

04 — Line Height & Leading Trim

이 문서가 답하는 질문: 디자인 시안에서 글자 위아래로 정확히 16px 패딩을 줬는데 코드로 옮기면 위가 더 비어 보이는 이유는 무엇인가? 그리고 그걸 어떻게 수학적으로 제거하는가? 한 줄 답 (Pyramid Top): line-height글자 위아래로 만드는 빈 공간(half-leading) 때문이다 — 이걸 해결하는 것이 text-box-trim(CSS 표준, Chrome 133+)과 그 폴리필인 Capsize 라이브러리다. 디자인 시스템의 모든 타이포 토큰은 font-size + line-height + letter-spacing3-튜플로 정의해야 한다.


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 기준

기준FigmaCSS
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 overridescale 불가

How — line-height 토큰화

1) Role 기반 line-height

용도line-height비율 (font-size 16 기준)
display1.1 ~ 1.2큰 글자는 tight 해야 시각적 결속
heading1.2 ~ 1.3적당한 tight
body1.5 ~ 1.6가독성의 default
caption1.4작은 글자는 느슨하게
code1.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-trimtrim-both / trim-start / trim-end / none위/아래/양쪽의 half-leading 제거
text-box-edgecap alphabetic / ex alphabetic / text어느 메트릭을 가장자리로 볼 것인가

text-box-edge

위쪽 기준아래쪽 기준용도
text폰트의 ascenderdescenderdefault (큰 차이 없음)
cap alphabeticcap-heightbaselineUI 표준 (Helvetica I·H 의 윗선↔baseline)
ex alphabeticx-heightbaseline본문에서 시각적 결속 강함

브라우저 지원

브라우저지원
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' }
// }

원리

  1. 폰트 메트릭에서 cap-height ratio 추출 (예: Inter는 0.728).
  2. (ascent + descent - cap-height) / 2 = 위쪽 trim 양.
  3. ::before / ::afternegative 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-height1996년 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-spacing 3-튜플로 토큰화.
  • line-heightunitless (1.5, 1.6).
  • 모던: text-box-trim: trim-both (Chrome 133+, Safari 18.2+).
  • 폴백: Capsize의 negative margin 트릭.
  • button·heading에만 적용. body 본문은 그대로 두는 게 가독성에 좋음.