타이포
rem 단일 축의 타이포 스케일.
font family
| 키 | 쓰임 |
|---|---|
sans | 본문 · UI. 한국어 권위 + Latin 보조(Pretendard) |
serif | 헤드라인 · 매거진 마스트헤드(Instrument Serif) |
mono | 메타 · 라벨 · 코드. 매거진 콜로폰 톤(JetBrains Mono) |
text style — 역할 하나로 고른다
크기 · 행간 · 자간을 따로 고르는 대신 역할 이름 하나를 고릅니다.
이번 주말, 홍대
금요일 밤의 라인업
크기 하나가 아니라 역할 하나를 고릅니다. 행간과 자간이 그 역할에 맞게 같이 따라오므로, 같은 자리에 선 글자는 지면이 달라도 같은 리듬으로 읽힙니다.
보조 문장은 한 단 아래에서 같은 규칙을 따릅니다.
import { text } from '@coldsurfers/surf-ui'import { sprinkles } from '@coldsurfers/surf-ui/sprinkles'export default function Example() { return ( <div style={{ display: 'grid', gap: 16, width: '100%' }}> <h2 className={text('heading', { weight: 'semibold' })}>이번 주말, 홍대</h2> <h3 className={text('title', { weight: 'medium' })}>금요일 밤의 라인업</h3> <p className={text('body')}> 크기 하나가 아니라 역할 하나를 고릅니다. 행간과 자간이 그 역할에 맞게 같이 따라오므로, 같은 자리에 선 글자는 지면이 달라도 같은 리듬으로 읽힙니다. </p> {/* 램프는 색을 묶지 않는다 — 같은 역할이 표면에 따라 다른 색을 입는 건 정상이다. */} <p className={`${text('bodySm')} ${sprinkles({ color: 'textSecondary' })}`}> 보조 문장은 한 단 아래에서 같은 규칙을 따릅니다. </p> <div style={{ display: 'flex', gap: 12, alignItems: 'baseline', flexWrap: 'wrap' }}> <span className={text('label', { weight: 'medium' })}>라벨</span> <span className={`${text('labelSm')} ${sprinkles({ color: 'textSecondary' })}`}> 21:00 입장 </span> <span className={`${text('micro')} ${sprinkles({ color: 'textTertiary' })}`}>ISSUE 07</span> </div> </div> )}import { text } from '@coldsurfers/surf-ui'
text('body')
text('label', { weight: 'medium' })| 이름 | fontSize | lineHeight | letterSpacing | 자리 |
|---|---|---|---|---|
heading | 2xl | tight | tight | 섹션 제목 |
title | xl | snug | normal | 카드 · 블록 제목 |
body | base | relaxed | normal | 읽는 본문 |
bodySm | sm | normal | normal | 보조 본문 · UI 문장 |
label | xs | snug | normal | 라벨 · 메타 |
labelSm | 2xs | snug | normal | 좁은 라벨 · 수치 |
micro | 3xs | snug | normal | 마이크로 라벨 |
왜 합성인가. 크기만 토큰이고 행간 · 자간을 호출부가 각자 정하면 같은 역할의 글자가 지면마다
다르게 섭니다. 실측에서 fontSize 가 들어간 sprinkles 호출 478건에 구별되는 조합이 46가지였고,
그중 대다수가 lineHeight 를 비운 채였습니다 — 비우면 상속값이 들어오니 정해진 적 없는 행간이
화면에 서는 셈입니다.
fontWeight 는 묶지 않습니다. 같은 크기에 굵기가 2~3종씩 붙어서, 이름에 넣으면 7단계가
21개가 됩니다. 그건 램프가 아니라 목록입니다. 색도 같은 이유로 밖에 둡니다.
램프 밖 크기가 필요하면 덮지 말고 escape hatch 를 씁니다 — sprinkles({ fontSize: 'lg' }).
램프 위에 크기만 덮으면 행간 · 자간이 그 크기에 맞지 않은 채 남습니다. lg(18px)에 단계를 주지
않은 것도 같은 이유입니다 — 실사용 8건이 body 와 title 사이에 흩어져 역할이 서지 않았습니다.
text() 는 ds-components 레이어라, 호출자가 얹는 sprinkles 유틸이 항상 이깁니다.
바닥을 램프로 깔고 한 속성만 유틸로 덮는 것이 성립합니다.
font size
rem 단일 축입니다. 값은 전부 16px root 기준 정수 px 등가입니다. px 로 직접 적지 않는 이유는 사용자의 브라우저 기본 글자 크기 설정을 따라가야 해서입니다.
--font-size-3xs0.625rem--font-size-2xs0.6875rem--font-size-xs0.75rem--font-size-sm0.875rem--font-size-base1rem--font-size-lg1.125rem--font-size-xl1.25rem--font-size-2xl1.5rem--font-size-3xl1.875rem--font-size-4xl2.25rem--font-size-5xl3rem--font-size-6xl3.75rem3xs · 2xs 는 라벨 축입니다. 실측에서 911px 사용의 uppercase 4168% · tracking 6988% ·
mono 지배가 12px 위와 뚜렷하게 갈렸고, 파일당 12종만 쓰여 역할이 실재했습니다.
반면 12.517px 은 한 파일이 35종을 섞어 쓰는 드리프트라 단계를 늘리지 않고 기존 네 단계
(xs · sm · base · lg)로 접었습니다.
이름에 값을 적어 넣지 않습니다. 13 · 15 같은 숫자 키를 거부한 이유는 하나입니다 — 토큰의
값을 바꿀 때 사용처를 전부 고쳐야 하면 간접화가 0 이라 그건 토큰이 아닙니다.
line height
--line-height-tight1.2--line-height-snug1.4--line-height-normal1.6--line-height-relaxed1.75font weight
--font-weight-light300--font-weight-regular400--font-weight-medium500--font-weight-semibold600--font-weight-bold700--font-weight-black900letter spacing
본문·UI 축입니다. 세 단계뿐이고, 크기가 아니라 역할로 고릅니다.
--letter-spacing-none0--letter-spacing-normal-0.02em--letter-spacing-tight-0.03em서울의 공연을 하나의 지면에서 발견합니다
서울의 공연을 하나의 지면에서 발견합니다
위 none · 아래 normal이번 주말, 홍대
2026-09-11 · 21:00 · KRW 25,000import { sprinkles } from '@coldsurfers/surf-ui/sprinkles'const SENTENCE = '서울의 공연을 하나의 지면에서 발견합니다'export default function Example() { return ( <div style={{ display: 'grid', gap: 24, width: '100%' }}> {/* 본문 — 한글은 0 에 두면 헐렁하다. 같은 문장을 두 줄로 나란히 둬야 차이가 보인다. */} <div style={{ display: 'grid', gap: 6 }}> <p className={sprinkles({ fontSize: 'base', color: 'textPrimary', letterSpacing: 'none' })}> {SENTENCE} </p> <p className={sprinkles({ fontSize: 'base', color: 'textPrimary', letterSpacing: 'normal' })} > {SENTENCE} </p> <span className={sprinkles({ fontSize: '2xs', fontFamily: 'mono', color: 'textSecondary' })} > 위 none · 아래 normal </span> </div> {/* 헤드라인 — 큰 글자는 더 조여야 같은 무게로 읽힌다. */} <h3 className={sprinkles({ fontSize: '3xl', fontWeight: 'semibold', color: 'textPrimary', lineHeight: 'tight', letterSpacing: 'tight', })} > 이번 주말, 홍대 </h3> {/* mono — 격자가 곧 리듬이라 조이지 않는다. */} <code className={sprinkles({ fontSize: 'sm', fontFamily: 'mono', letterSpacing: 'none' })}> 2026-09-11 · 21:00 · KRW 25,000 </code> </div> )}sprinkles({ letterSpacing }) 로 얹거나, .css.ts 에서 vars.letterSpacing.normal 을 읽습니다.
어느 쪽이든 같은 --letter-spacing-* 변수를 가리킵니다.
이 축이 왜 필요했는지: 아래 editorialType 3그룹만 letterSpacing 을 갖고 있어서, 본문·UI 는
브라우저 기본값(0)이었습니다. Pretendard 로 한글을 0 에 두면 글자가 느슨하게 벌어져 같은
크기에서 라틴보다 헐렁하게 읽힙니다. 그래서 소비처가 자기 CSS 로 이 자리를 메우고 있었습니다.
큰 글자는 더 조여야 같은 무게로 읽히므로 24px 이상 헤드라인은 tight 로 갑니다. mono 는
none(0) 입니다 — 고정폭 글리프는 격자가 곧 리듬이라, 조이면 코드·수치가 뭉칩니다.
editorialType 의 자간과 겹쳐 쓰지 않습니다. 저쪽은 크기까지 묶은 합성 슬롯(clamp display ·
uppercase eyebrow)이고 이쪽은 어느 크기에든 얹는 단일 속성입니다. 합성 슬롯을 쓰는 자리에서는
그쪽 값이 이미 자간을 정하므로 이 토큰을 덧대지 않습니다.
text box
알약 · 칩처럼 상자 안 한 줄 글자를 눈으로 가운데에 세울 때 씁니다.
<span className={pill}>
<span className={sprinkles({ display: 'block', textBox: 'cap' })}>새 11</span>
</span>textBox: 'cap' 은 글자 상자를 대문자 높이에서 기준선까지 자릅니다(text-box: trim-both cap alphabetic).
Pretendard 는 위 여백이 아래보다 커서, 상자만 flex 로 가운데 두면 글자가 0.5–1px 뜹니다.
패딩 1px 로 메우면 크기가 바뀔 때마다 다시 재야 합니다.
flex · grid 상자 자신에는 먹지 않습니다. 안쪽 블록 한 겹에 겁니다. 지원하지 않는 브라우저(Firefox)에서는 자르지 않은 원래 상자로 남습니다 — 깨지지는 않습니다.
editorial type
매거진 톤의 합성 타이포 슬롯입니다. eyebrow · display · caption 세 그룹이 있고,
letterSpacing 과 lineHeight 만 자기 값을 갖습니다 — 크기는 위 fontSize 를 읽습니다.
그 슬롯을 컴포넌트로 쓰는 쪽이 Eyebrow 입니다.