06 — Responsive Type & Fluid Scale
이 문서가 답하는 질문: “모바일에서는 14px, 데스크톱에서는 18px”을 더 이상 미디어 쿼리 5개로 표현하지 않는다. 그럼 어떻게? 그리고 그 방식이 사용자의 브라우저 줌과 충돌하지 않으려면? 한 줄 답 (Pyramid Top):
clamp(min, preferred, max)한 줄이 연속적 fluid type을 만든다 — 그리고 그 비례 기준을vw(viewport) 가 아니라cqi(container inline) 로 옮기면 같은 컴포넌트가 어느 컨테이너에 들어가도 그 폭에 맞춰 자동 스케일된다. 핵심은 모든 단위는rem—px로 잠그면 사용자 폰트 크기 설정이 무력화된다.
Why — 미디어 쿼리 폭발 + 접근성 줌
1) 전통 미디어 쿼리의 한계
/* 5개의 불연속 점프 */
h1 { font-size: 24px; }
@media (min-width: 480px) { h1 { font-size: 28px; } }
@media (min-width: 768px) { h1 { font-size: 36px; } }
@media (min-width: 1024px) { h1 { font-size: 44px; } }
@media (min-width: 1440px) { h1 { font-size: 56px; } }- 481px에서 갑자기 28→36 점프. 사이 사이즈는 불가.
- 같은 컴포넌트가 어느 콘텐츠 컬럼에 들어갔는지를 모름 — sidebar(300px)와 main(900px) 둘 다 viewport 1024px에서는 같은 사이즈.
- 폰트 사이즈마다 이걸 반복 → CSS가 불어남.
2) px 고정의 접근성 위반
.body { font-size: 14px; } /* ❌ */- 사용자가 브라우저 폰트 크기를 크게 설정해도 안 바뀜.
- WCAG 2.2 의 resize 기준 (200%까지 확대 가능해야 함) 위반.
- 노안·저시력 사용자 차단.
| 풀려는 문제 | 미디어쿼리 | clamp() | container query |
|---|---|---|---|
| 사이 사이즈 | 불가 | 연속 | 연속 |
| 컨테이너 인식 | 불가 | 불가 (viewport만) | 가능 |
| 미디어쿼리 수 | 4~6개 | 0개 | 0개 |
| 접근성 줌 | px면 깨짐 | rem 사용 시 OK | rem 사용 시 OK |
How — clamp() 3-인자 공식
font-size: clamp(MIN, PREFERRED, MAX);- MIN: 절대 이 아래로 안 내려감.
- PREFERRED: 이상값 (보통
vw또는cqi기반 동적 값). - MAX: 절대 이 위로 안 올라감.
기본 패턴 (viewport 기반)
:root {
font-size: clamp(1rem, 0.5rem + 1vw, 1.25rem);
}- 320px 화면:
0.5rem + 1vw = 8 + 3.2 = 11.2px→ MIN1rem (16px)적용. - 1280px:
0.5rem + 1vw = 8 + 12.8 = 20.8px→ MAX1.25rem (20px)적용. - 사이: 연속 보간.
공식 유도
원하는 결과: 320px → 16px, 1280px → 20px.
font-size = a × vw + b × rem
at vw=320: 320a + 16b = 16
at vw=1280: 1280a + 16b = 20
→ 960a = 4
→ a = 0.00417 = 0.417vw
→ b × 16 = 16 - 320 × 0.00417 = 14.67
→ b = 0.917rem→ font-size: clamp(1rem, 0.917rem + 0.417vw, 1.25rem)
더 단순한 자동 계산 도구
- utopia.fyi — viewport 범위 + 스케일 ratio 입력 → 토큰 자동 생성.
- fluid.tw — Tailwind 클래스 자동.
What — Container query 기반 fluid (cqi)
2023년 표준화. 부모 컨테이너의 폭을 기준으로 스케일.
.card {
container-type: inline-size; /* 이 element가 *컨테이너* 가 됨 */
}
.card-title {
font-size: clamp(1rem, 0.5rem + 4cqi, 2rem);
/* cqi = container's inline size의 1% */
}동작
- card가 300px 폭:
0.5rem + 4cqi = 8 + 12 = 20px→ 토큰 사이. - 같은 card가 800px 폭:
0.5rem + 4cqi = 8 + 32 = 40px→ MAX2rem (32px).
viewport vs container 비교
| 기준 | vw | cqi |
|---|---|---|
| 비례 단위 | viewport 1% | container 1% |
| 같은 컴포넌트, 다른 위치 | 같은 크기 | 다른 크기 (컨테이너별) |
| 적합 | 페이지 전체 헤더 | 재사용 가능한 카드·컴포넌트 |
| 브라우저 지원 | 100%+ | Chrome 105+ / Safari 16+ / Firefox 110+ |
What — Fluid type token 시스템
Utopia-style fluid scale
:root {
/* viewport 320~1240 사이에서 fluid */
--step--2: clamp(0.6944rem, 0.6597rem + 0.1736vw, 0.8rem); /* ~11~13px */
--step--1: clamp(0.8333rem, 0.7857rem + 0.2381vw, 0.9722rem);
--step-0: clamp(1rem, 0.9348rem + 0.3261vw, 1.2153rem); /* base */
--step-1: clamp(1.2rem, 1.1115rem + 0.4423vw, 1.5191rem);
--step-2: clamp(1.44rem, 1.3211rem + 0.5946vw, 1.8989rem);
--step-3: clamp(1.728rem, 1.5694rem + 0.7929vw, 2.3737rem);
--step-4: clamp(2.0736rem,1.8623rem + 1.0566vw, 2.9671rem);
--step-5: clamp(2.4883rem,2.2074rem + 1.4042vw, 3.7089rem);
}
h1 { font-size: var(--step-5); }
h2 { font-size: var(--step-4); }
p { font-size: var(--step-0); }Panda CSS 적용
theme: {
tokens: {
fontSizes: {
// static
sm: { value: "0.875rem" },
base: { value: "1rem" },
lg: { value: "1.25rem" },
// fluid
"fluid.sm": { value: "clamp(0.875rem, 0.85rem + 0.125vw, 1rem)" },
"fluid.base": { value: "clamp(1rem, 0.95rem + 0.25vw, 1.125rem)" },
"fluid.lg": { value: "clamp(1.125rem, 1.05rem + 0.375vw, 1.375rem)" },
"fluid.xl": { value: "clamp(1.25rem, 1.1rem + 0.75vw, 1.875rem)" },
"fluid.2xl": { value: "clamp(1.5rem, 1.25rem + 1.25vw, 2.5rem)" },
"fluid.3xl": { value: "clamp(2rem, 1.5rem + 2.5vw, 3.75rem)" }
}
}
}fluid는 hero·display에만. 본문은 static rem이 가독성에 좋다 (점진적 변동이 읽기에 산만).
What — 접근성 (A11y) — rem 우선
rem의 의미
1rem= root element의 font-size.- 사용자가 브라우저 설정에서 기본 폰트 크기를 18px로 바꾸면
1rem = 18px이 됨. px은 고정 — 사용자 설정 무시.
잘못된 패턴
/* ❌ */
html { font-size: 16px; } /* 절대 px 지정 — 사용자 설정 덮어씀 */
.body { font-size: 14px; }올바른 패턴
/* ✅ */
html {
/* font-size 지정 안 함 → browser default (보통 16px) */
}
.body {
font-size: 0.875rem; /* 14px @ 16px base. 사용자가 20px base면 17.5px */
}clamp + rem + vw 결합 — 접근성 안전
/* WCAG 안전한 fluid */
h1 {
font-size: clamp(1.5rem, 1rem + 2vw, 3rem);
/* 모든 인자가 rem 또는 viewport 단위 → 사용자 줌 시 *모두 스케일* */
}
/* ❌ px이 섞이면 위험 */
h1 {
font-size: clamp(24px, 16px + 2vw, 48px); /* 사용자 줌 무시 */
}Tailwind v4의 단위 정책
Tailwind v4부터 기본 단위가 rem. text-base = 1rem. 이게 접근성 best practice 와 일치.
What — Tailwind v4 fluid + container query
{/* container query */}
<div class="@container">
<h1 class="@sm:text-2xl @md:text-3xl @lg:text-5xl">
제목
</h1>
</div>@container=container-type: inline-size.@sm:,@md:,@lg:: 컨테이너의 내부 폭 기준 (viewport 아님).
임의 clamp
<h1 class="text-[clamp(1.5rem,_1rem_+_2vw,_3rem)]">
Hero
</h1>또는 토큰화:
@theme {
--text-fluid-hero: clamp(1.5rem, 1rem + 2vw, 3rem);
}<h1 class="text-fluid-hero">Hero</h1>What-if — 잘못 쓰면 어떻게 깨지는가
- 함정 1:
px로 clamp 작성 —clamp(16px, 1vw + 8px, 24px). 사용자 200% 줌 시 글자가 그대로. 대응: 모든 인자를 *rem또는em*으로. - 함정 2: fluid를 본문에 적용 — 본문 폰트가 스크롤·resize 시 미세하게 변동하면 읽기가 산만. 대응: fluid는 display/hero/heading만.
- 함정 3: 너무 가파른 fluid 기울기 —
clamp(1rem, 5vw, 4rem)— viewport 5%면 모바일→데스크톱 사이 4배. 모바일에선 너무 작고, 데스크톱에선 너무 큼. 대응:0.5~1.5vw범위 권장. - 함정 4: container query 부모 누락 —
cqi사용했는데 부모에container-type없음 → 0으로 계산. 대응: 항상 컨테이너 명시. - 함정 5:
cqi+ nested container — 안쪽 컨테이너의cqi가 어느 부모 기준인지 헷갈림. 대응:container-name으로 명시 (container-name: card, 사용 시container: card / inline-size). - 함정 6: 사용자가
prefers-reduced-motion인데 fluid가 resize 시 transition — 애니메이션 멀미 유발 가능. 대응: clamp 자체는 transition 없는 즉시 적용이라 보통 안전. 다만 width animation은 따로 가드.
Insight — cqi가 진짜 게임 체인저인 이유
clamp() + vw 패턴은 2020년경 Mike Riethmuller와 Utopia가 대중화했다. 하지만 2년 뒤에 등장한 cqi가 진짜 패러다임을 바꿨다.
디자인 시스템에서의 충격
이전: 같은 Card 컴포넌트가 sidebar(300px) 에 들어가도, main column(900px) 에 들어가도 같은 헤딩 크기. 디자이너는 두 가지 variant(Card.compact, Card.spacious)를 만들어 동시에 관리해야 했음.
이후: cqi 한 줄로 card 자체가 자기 컨테이너 폭을 알고 스케일. variant가 불필요해짐.
진짜 재사용 가능한 컴포넌트가 가능해짐
/* 컴포넌트 1개. 어떤 컨테이너에 넣어도 자동 적응 */
.card {
container-type: inline-size;
}
.card-title {
font-size: clamp(1rem, 4cqi, 2rem);
}
.card-padding {
padding: clamp(0.5rem, 3cqi, 2rem);
}→ 디자인 시스템 컴포넌트의 “한 번 만들면 어디서나 동작” 약속을 비로소 실현. 이게 2024년 디자인 시스템 트렌드가 Component variants 줄이기 + container-aware로 옮겨가는 이유.
한계
container query는 그 element가 layout flow 안의 부모를 보지만, grid/flex의 형제 widths를 보지 못함. 즉 truly content-aware layout(글자 길이에 따라 사이즈)은 아직 style query(2025년 표준화 중)나 JS measurement가 필요.
요약
- 미디어쿼리 대신
clamp(min, preferred, max)한 줄로 연속적 fluid type. - 비례 단위:
vw(viewport) →cqi(container) 로 옮기는 게 재사용 가능한 컴포넌트의 핵심. - 모든 단위는
rem—px는 접근성 줌을 차단. - fluid는 hero/heading 전용. 본문은 static rem.
container-type: inline-size를 부모에 명시해야cqi동작.