surf-uiby COLDSURF
Foundations

타이포

rem 단일 축의 타이포 스케일.

font family

키쓰임
sans본문 · UI. 한국어 권위 + Latin 보조(Pretendard)
serif헤드라인 · 매거진 마스트헤드(Instrument Serif)
mono메타 · 라벨 · 코드. 매거진 콜로폰 톤(JetBrains Mono)

text style — 역할 하나로 고른다

크기 · 행간 · 자간을 따로 고르는 대신 역할 이름 하나를 고릅니다.

이번 주말, 홍대

금요일 밤의 라인업

크기 하나가 아니라 역할 하나를 고릅니다. 행간과 자간이 그 역할에 맞게 같이 따라오므로, 같은 자리에 선 글자는 지면이 달라도 같은 리듬으로 읽힙니다.

보조 문장은 한 단 아래에서 같은 규칙을 따릅니다.

라벨21:00 입장ISSUE 07
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' })
이름fontSizelineHeightletterSpacing자리
heading2xltighttight섹션 제목
titlexlsnugnormal카드 · 블록 제목
bodybaserelaxednormal읽는 본문
bodySmsmnormalnormal보조 본문 · UI 문장
labelxssnugnormal라벨 · 메타
labelSm2xssnugnormal좁은 라벨 · 수치
micro3xssnugnormal마이크로 라벨

왜 합성인가. 크기만 토큰이고 행간 · 자간을 호출부가 각자 정하면 같은 역할의 글자가 지면마다 다르게 섭니다. 실측에서 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 로 직접 적지 않는 이유는 사용자의 브라우저 기본 글자 크기 설정을 따라가야 해서입니다.

Ag--font-size-3xs0.625rem
Ag--font-size-2xs0.6875rem
Ag--font-size-xs0.75rem
Ag--font-size-sm0.875rem
Ag--font-size-base1rem
Ag--font-size-lg1.125rem
Ag--font-size-xl1.25rem
Ag--font-size-2xl1.5rem
Ag--font-size-3xl1.875rem
Ag--font-size-4xl2.25rem
Ag--font-size-5xl3rem
Ag--font-size-6xl3.75rem

3xs · 2xs 는 라벨 축입니다. 실측에서 911px 사용의 uppercase 4168% · tracking 6988% · mono 지배가 12px 위와 뚜렷하게 갈렸고, 파일당 12종만 쓰여 역할이 실재했습니다.

반면 12.517px 은 한 파일이 35종을 섞어 쓰는 드리프트라 단계를 늘리지 않고 기존 네 단계 (xs · sm · base · lg)로 접었습니다.

이름에 값을 적어 넣지 않습니다. 13 · 15 같은 숫자 키를 거부한 이유는 하나입니다 — 토큰의 값을 바꿀 때 사용처를 전부 고쳐야 하면 간접화가 0 이라 그건 토큰이 아닙니다.

line height

Ag--line-height-tight1.2
Ag--line-height-snug1.4
Ag--line-height-normal1.6
Ag--line-height-relaxed1.75

font weight

Ag--font-weight-light300
Ag--font-weight-regular400
Ag--font-weight-medium500
Ag--font-weight-semibold600
Ag--font-weight-bold700
Ag--font-weight-black900

letter spacing

본문·UI 축입니다. 세 단계뿐이고, 크기가 아니라 역할로 고릅니다.

Ag--letter-spacing-none0
Ag--letter-spacing-normal-0.02em
Ag--letter-spacing-tight-0.03em

서울의 공연을 하나의 지면에서 발견합니다

서울의 공연을 하나의 지면에서 발견합니다

위 none · 아래 normal

이번 주말, 홍대

2026-09-11 · 21:00 · KRW 25,000
import { 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 입니다.

On this page