ご注意
この記事は AI のサポートを受けていますが
1. 課題:なぜ responsive typography は厄介なのか
こんな経験、ありませんか?
-
ケース1: デスクトップで H1
48px— きれい。モバイルだと画面の半分近くを占める — デザイナーからすぐ連絡が来る。@media (max-width: 768px) { h1 { font-size: 24px; } }を追加。タブレット 820px は? ラップトップ 1024px は? テストのたびに breakpoint が増え、CSS は膨らむのに文字は ジャンプ するだけで 滑らかに伸縮 しない — エレベーターで1階ずつ移動するようなものです。 -
ケース2:
vwで font-size を伸縮 — エレガントに聞こえます。4K モニターで開くと、広告バナー級の文字サイズ。ウィンドウを 320px に縮めると、論文の脚注くらい小さい。素のvwには下限も上限もない — 伸縮はするが、止まることを知らない。 -
ケース3:
clamp()を入れて完了、と思った。QA から「200% ズームで H1 が大きくならない — むしろ小さくなる」と報告。私もこのバグの trace に半日かかったことがあります。ズーム時にpreferred内のvwが再計算され、font-size が逆方向に動く。fluid type はモダンに聞こえますが、accessible とは限りません。 -
ケース4: H1 から caption まで一貫した type scale が欲しい — 各レベルが mobile と desktop の間で滑らかに伸縮する。各レベルの
clamp()を手計算... calculator は友達ですが、指は疲れます。
要点: 「文字が崩れたら media query を足す」responsive typography は設計ではなく 火消し です。そして火消しを繰り返しても、また燃えます。
2. 変化をイメージする:viewport に応じた font-size
コードに入る前に、下の表でイメージを掴んでください。body text が viewport 320px で 16px、1200px で 24px まで 連続的に — ジャンプなく — 伸縮する場合:
| Viewport width | Font-size (px) | 備考 |
|---|---|---|
| 320px | 16.0 | 開始点 (min) |
| 480px | 17.5 | 滑らかに伸縮 |
| 640px | 19.0 | 滑らかに伸縮 |
| 768px | 20.0 | 滑らかに伸縮 |
| 1024px | 22.4 | 滑らかに伸縮 |
| 1200px | 24.0 | 終了点 (max) |
viewport が1px 縮んだだけで 20px から 16px に「ドン」と落ちる — そんなことはありません。読み心地がずっと滑らかになる。それが fluid typography の目指すところです。
3. 本質:Fluid Typography とは
Fluid Typography(responsive typography、flexible type — 呼び方は何でも)は、breakpoint でパッと変わるのではなく、viewport に合わせて文字サイズが 滑らかに 伸縮する手法です。
3.1. Media queries — おなじみだが限界あり
media query は breakpoint ごとに文字サイズを決めさせます。breakpoint の間は文字は 動きません。
/* 従来: 3 breakpoint、3 値 */
h1 {
font-size: 48px; /* desktop */
}
@media (max-width: 1024px) {
h1 {
font-size: 36px;
}
}
@media (max-width: 768px) {
h1 {
font-size: 28px;
}
}
@media (max-width: 480px) {
h1 {
font-size: 20px;
}
}
問題:481px〜767px の間は 28px のまま。480px になった瞬間 — ドン — 20px。体験はカクカク、コードは冗長。
3.2. viewport 単位 (vw) — 良さそうだが注意
vw(1vw = viewport 幅の 1%)なら font-size が連続的に伸縮します:
h1 {
font-size: 5vw; /* viewport 幅の 5% */
}
欠点:上限も下限もない。4K(3840px)→ 文字 192px、読めません。320px → 16px、近眼じゃないと厳しい。床と天井が必要です。
3.3. clamp() — 第三の選択肢
clamp(min, preferred, max) は三者をまとめます:下限(min)、上限(max)、その間の伸縮値(preferred — 多くは rem + vw)。1行で複数の media query を置き換え、vw の暴走も防げます。
構文の詳細、media query との比較、実践パターンは セクション4 へ。
4. clamp() — 構文・比較・実践パターン
4.1. 構文と動き
3つの引数 — min〜max の範囲の中に「カーブ」があるイメージ:
clamp(minimum, preferred, maximum)
| 引数 | 意味 | 例 |
|---|---|---|
minimum |
最小値 |
1rem, 16px, 1.5rem
|
preferred |
伸縮する値 |
2.5vw, calc(1rem + 1vw), 5cqw
|
maximum |
最大値 |
3rem, 48px, 4rem
|
clamp(MIN, VAL, MAX) は max(MIN, min(VAL, MAX)) と同等 — ブラウザが範囲内の値を選びます。
H1 の例:
h1 {
font-size: clamp(1.5rem, 5vw, 4rem);
}
- 小さい画面:
1.5rem未満にはならない - 大きい画面:
4remを超えない - その間:
5vwで伸縮
4.2. clamp() vs Media Queries — さっと比較
同じ H1、2つの書き方:
/* 方法1: 3 つの media query */
h1 {
font-size: 2rem; /* 32px - mobile */
}
@media (min-width: 768px) {
h1 {
font-size: 2.5rem;
} /* 40px - tablet */
}
@media (min-width: 1200px) {
h1 {
font-size: 3rem;
} /* 48px - desktop */
}
/* 方法2: clamp() 1行 — 私はこちらをよく使います */
h1 {
font-size: clamp(2rem, 2.5vw + 1rem, 3rem);
}
4.3. clamp() は font-size だけじゃない
padding、gap、width… どれでも使えます — typography と spacing が同じ「伸縮のリズム」で動きます:
.article {
font-size: clamp(1rem, 2vw + 0.5rem, 1.5rem);
padding: clamp(1rem, 3vw, 3rem);
gap: clamp(0.5rem, 1.5vw, 2rem);
width: clamp(200px, 50%, 600px);
}
line-height について: 多くのプロジェクトでは fluid font-size だけで十分です。unitless の固定 line-height(例:1.5)の方が fluid line-height より保守しやすく、結果も安定します。font-size に合わせて line-height も変えたい場合は慎重に — vertical rhythm が崩れやすいです。
5. 計算式 A から Z まで
ここは少し数学 — 一度理解すれば、type scale 全体を当てずっぽうで作る必要がなくなります。
5.1. 基本の問題
こうしたいとします:
- 画面 320px(mobile):font-size = 16px(1rem)
- 画面 1200px(desktop):font-size = 24px(1.5rem)
- 2点の間で font-size が 線形に 伸縮
5.2. 一般式
viewport → font-size の2点をアンカーに:
-
minFontSize(px)@minViewport(px) -
maxFontSize(px)@maxViewport(px)
slope = (maxFontSize - minFontSize) / (maxViewport - minViewport)
intercept = minFontSize - slope * minViewport
/* rem (1rem = 16px) と vw に変換 */
preferred = (slope * 100)vw + (intercept / 16)rem
例:min=320px→16px、max=1200px→24px
slope = (24 - 16) / (1200 - 320) = 8/880 ≈ 0.00909
intercept = 16 - 0.00909*320 ≈ 13.09px = 0.818rem
preferred = 0.909vw + 0.818rem
=> font-size: clamp(1rem, 0.909vw + 0.818rem, 1.5rem);
5.3. 各 viewport での値
| Viewport width | 0.909vw + 0.818rem |
結果 (px) |
|---|---|---|
| 320px | 0.909*3.2 + 13.09 | ~16.0 |
| 480px | 0.909*4.8 + 13.09 | ~17.5 |
| 640px | 0.909*6.4 + 13.09 | ~19.0 |
| 768px | 0.909*7.68 + 13.09 | ~20.0 |
| 1024px | 0.909*10.24 + 13.09 | ~22.4 |
| 1200px | 0.909*12 + 13.09 | ~24.0 |
均等に増加、ジャンプなし — セクション2の表と一致します。
5.4. ツール — 手計算はやめましょう
無料の calculator がやってくれます:
- Fluid Typography Calculator
- Clamp Generator
- Utopia.fyi — type scale と space scale の両方
6. 伸縮する type scale システムの構築
h1、h2… ごとにバラバラに clamp() を書くと、どの式がどのレベルか忘れます。CSS custom properties にまとめましょう — 1か所直せばサイト全体が変わります。
下の token は Utopia.fyi 風:xs から 4xl まで、viewport 320px → 1200px。
token 一式(`:root` + fluid spacing)
:root {
--font-size-xs: clamp(
0.75rem,
0.659vw + 0.614rem,
0.875rem
); /* 12px → 14px */
--font-size-sm: clamp(0.875rem, 0.727vw + 0.727rem, 1rem); /* 14px → 16px */
--font-size-base: clamp(1rem, 0.795vw + 0.841rem, 1.125rem); /* 16px → 18px */
--font-size-md: clamp(
1.125rem,
0.909vw + 0.932rem,
1.25rem
); /* 18px → 20px */
--font-size-lg: clamp(1.25rem, 1.136vw + 0.977rem, 1.5rem); /* 20px → 24px */
--font-size-xl: clamp(1.5rem, 1.477vw + 1.091rem, 1.875rem); /* 24px → 30px */
--font-size-2xl: clamp(
1.875rem,
1.932vw + 1.273rem,
2.25rem
); /* 30px → 36px */
--font-size-3xl: clamp(
2.25rem,
2.386vw + 1.455rem,
2.75rem
); /* 36px → 44px */
--font-size-4xl: clamp(2.75rem, 3.068vw + 1.636rem, 3.5rem); /* 44px → 56px */
/* Fluid spacing */
--space-xs: clamp(0.25rem, 0.159vw + 0.227rem, 0.375rem);
--space-sm: clamp(0.5rem, 0.318vw + 0.455rem, 0.75rem);
--space-md: clamp(1rem, 0.636vw + 0.909rem, 1.5rem);
--space-lg: clamp(1.5rem, 0.955vw + 1.364rem, 2.25rem);
--space-xl: clamp(2rem, 1.273vw + 1.818rem, 3rem);
}
semantic HTML に token を割り当て — デザイナーが「H1 は 4xl」と言えば、dev は px を覚える必要なし:
h1 {
font-size: var(--font-size-4xl);
}
h2 {
font-size: var(--font-size-3xl);
}
h3 {
font-size: var(--font-size-2xl);
}
h4 {
font-size: var(--font-size-xl);
}
h5 {
font-size: var(--font-size-lg);
}
h6 {
font-size: var(--font-size-md);
}
p,
.body {
font-size: var(--font-size-base);
}
small,
.caption {
font-size: var(--font-size-sm);
}
7. React + TypeScript:Typography System
token ができたら production へ:CSS ファイル1つ + Text component — プロジェクトにコピーすれば動きます。
7.1. 完成版 CSS
:root {
--font-size-xs: clamp(0.75rem, 0.659vw + 0.614rem, 0.875rem);
--font-size-sm: clamp(0.875rem, 0.727vw + 0.727rem, 1rem);
--font-size-base: clamp(1rem, 0.795vw + 0.841rem, 1.125rem);
--font-size-md: clamp(1.125rem, 0.909vw + 0.932rem, 1.25rem);
--font-size-lg: clamp(1.25rem, 1.136vw + 0.977rem, 1.5rem);
--font-size-xl: clamp(1.5rem, 1.477vw + 1.091rem, 1.875rem);
--font-size-2xl: clamp(1.875rem, 1.932vw + 1.273rem, 2.25rem);
--font-size-3xl: clamp(2.25rem, 2.386vw + 1.455rem, 2.75rem);
--font-size-4xl: clamp(2.75rem, 3.068vw + 1.636rem, 3.5rem);
--space-xs: clamp(0.25rem, 0.159vw + 0.227rem, 0.375rem);
--space-sm: clamp(0.5rem, 0.318vw + 0.455rem, 0.75rem);
--space-md: clamp(1rem, 0.636vw + 0.909rem, 1.5rem);
--space-lg: clamp(1.5rem, 0.955vw + 1.364rem, 2.25rem);
--space-xl: clamp(2rem, 1.273vw + 1.818rem, 3rem);
}
html { font-size: 16px; -webkit-text-size-adjust: 100%; }
body { font-size: var(--font-size-base); line-height: 1.5; }
h1 { font-size: var(--font-size-4xl); line-height: 1.1; }
h2 { font-size: var(--font-size-3xl); line-height: 1.1; }
h3 { font-size: var(--font-size-2xl); line-height: 1.2; }
h4 { font-size: var(--font-size-xl); line-height: 1.2; }
h5 { font-size: var(--font-size-lg); line-height: 1.4; }
h6 { font-size: var(--font-size-md); line-height: 1.4; }
.text-xs { font-size: var(--font-size-xs); }
.text-sm { font-size: var(--font-size-sm); }
.text-base { font-size: var(--font-size-base); }
.text-md { font-size: var(--font-size-md); }
.text-lg { font-size: var(--font-size-lg); }
.text-xl { font-size: var(--font-size-xl); }
.text-2xl { font-size: var(--font-size-2xl); }
.text-3xl { font-size: var(--font-size-3xl); }
.text-4xl { font-size: var(--font-size-4xl); }
.font-normal { font-weight: 400; }
.font-medium { font-weight: 500; }
.font-semibold { font-weight: 600; }
.font-bold { font-weight: 700; }
.align-center { text-align: center; }
.align-right { text-align: right; }
7.2. React Component(TypeScript)
import React from 'react';
import './typography.css';
type TextVariant = 'xs' | 'sm' | 'base' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl';
interface TextProps {
children: React.ReactNode;
variant?: TextVariant;
as?: keyof JSX.IntrinsicElements;
className?: string;
weight?: 'normal' | 'medium' | 'semibold' | 'bold';
color?: string;
align?: 'left' | 'center' | 'right';
style?: React.CSSProperties;
}
export const Text: React.FC<TextProps> = ({
children,
variant = 'base',
as: Component = 'p',
className = '',
weight = 'normal',
color,
align = 'left',
style = {},
}) => {
const variantClass = `text-${variant}`;
const weightClass = `font-${weight}`;
const alignClass = align === 'left' ? '' : `align-${align}`;
const combinedStyle: React.CSSProperties = {
...(color && { color }),
...style,
};
return (
<Component
className={`${variantClass} ${weightClass} ${alignClass} ${className}`}
style={combinedStyle}
>
{children}
</Component>
);
};
7.3. 使ってみる
ウィンドウをリサイズ(または DevTools の responsive モード)— breakpoint 用の props なしで文字が伸縮します:
import React from 'react';
import { Text } from './Typography';
const App: React.FC = () => {
return (
<main style={{ padding: 'var(--space-lg)' }}>
<Text
variant="4xl"
weight="bold"
align="center"
style={{ marginBottom: 'var(--space-lg)' }}
>
実践での Fluid Typography
</Text>
<Text
variant="lg"
weight="medium"
style={{ marginBottom: 'var(--space-md)' }}
>
少し大きめのサブ見出し
</Text>
<Text variant="base" style={{ marginBottom: 'var(--space-sm)' }}>
通常の本文。文字サイズは 16px から 18px まで伸縮します。
</Text>
<div style={{ display: 'flex', gap: 'var(--space-md)' }}>
<Text variant="xs" style={{ color: '#666' }}>小さな注釈</Text>
<Text variant="sm" style={{ color: '#888' }}>Footnote</Text>
</div>
</main>
);
};
8. よくある落とし穴と対処法
clamp() は使いやすい反面、使い間違えやすい。私がよく踏むところ:
8.1. ⚠️ clamp() 内で px を使う — accessibility が死ぬ
/* ❌ 使わない */
h1 {
font-size: clamp(24px, 5vw, 48px);
}
ユーザーがブラウザのデフォルト font-size を変える(例:16px → 20px)と、px の clamp は追従しません。rem を使いましょう:
/* ✅ rem を使う */
h1 {
font-size: clamp(1.5rem, 5vw, 3rem);
}
8.2. ⚠️ ズームと viewport 単位 — 見落とされがちな罠
200% ズームで文字が大きくならない(むしろ小さくなる)? 驚かないでください — fluid type の定番 accessibility バグです:
原因:
対処:
- min と max は
rem— 例外なし。 -
vwは補助にとどめ、preferredはrem + vwを優先。 - merge 前に Chrome・Firefox・Safari で 200% ズーム — 目視だけは信用しない。
/* ✅ より安全 */
h1 {
font-size: clamp(1.5rem, 1rem + 2vw, 3rem);
}
8.3. ⚠️ iOS で -webkit-text-size-adjust を忘れる
iOS Safari はこの1行がないと、勝手に font-size をいじる癖があります:
html {
-webkit-text-size-adjust: 100%;
}
8.4. ⚠️ 全部に clamp() — 必要ないことも
fluid type は良いですが、どこでも伸縮が必要とは限りません:
- 見出し → 使う
- 本文 → 使う(伸縮幅は小さめで十分)
- キャプション / 小文字 → 固定
remも検討(もう小さいので、さらに縮むと読みにくい) - コード / 等幅 → 読みやすさのため固定が多い
9. アクセシビリティ(Accessibility)
Figma と 1440px viewport では fluid type はきれい — でも WCAG は Figma ではなく、200% ズームしたユーザーを見ます。
9.1. WCAG 1.4.4 Resize Text(Level AA)
ユーザーは、コンテンツや機能を失うことなく、テキストを 200% まで拡大できる。
clamp() では、ズーム時に font-size が まだ増えるか — max が低すぎて頭打ちになっていないかを確認します。
9.2. 2種類の token — どちらを使う?
| token セット | いつ使う | 特徴 |
|---|---|---|
| セクション6(Utopia 風) | フル design system(xs → 4xl + spacing) |
レベルが多く、desktop で伸縮がはっきり |
下記(rem + vw) |
a11y 監査、200% ズームで fail |
rem が主、vw は軽く押すだけ |
私のワークフロー:セクション6のセットで ship → ズームテスト → fail したら preferred を rem + vw に差し替え:
:root {
--font-size-base: clamp(1rem, 0.8rem + 0.4vw, 1.125rem);
--font-size-lg: clamp(1.125rem, 0.9rem + 0.6vw, 1.25rem);
--font-size-xl: clamp(1.25rem, 1rem + 0.8vw, 1.5rem);
--font-size-2xl: clamp(1.5rem, 1.2rem + 1vw, 1.875rem);
--font-size-3xl: clamp(1.875rem, 1.5rem + 1.2vw, 2.25rem);
--font-size-4xl: clamp(2.25rem, 1.8rem + 1.5vw, 3rem);
}
- min/max は
rem。vwにハンドルを渡さない。
9.3. QA 渡し前のチェックリスト
- 200%・400% ズーム — 文字は大きくなるか?
- OS の font-size(設定 → 大)— 追従するか?
- スクリーンリーダー — 内容は読めるか、切れていないか?
- コントラスト ≥ 4.5:1(WCAG AA)— 薄い文字色は fluid type では救えません。
10. Production Ready
もう少し — リリース前夜11時に browser support を Google しなくて済むように。
10.1. Browser Support
| 機能 | Chrome | Firefox | Safari |
|---|---|---|---|
clamp() |
✅ 79+ | ✅ 75+ | ✅ 13.1+ |
| Viewport units | ✅ 26+ | ✅ 19+ | ✅ 6+ |
| Container units | ✅ 105+ | ✅ 110+ | ✅ 16+ |
Internet Explorer: IE 11 は clamp() 非対応 です。まだ IE 対応が必要? 固定 font-size の fallback か polyfill — 幸運を祈ります。
clamp() と viewport units はモダンブラウザで問題なし。Container units(cqw、cqh、cqi)は新しめ — component 単位のタイポグラフィに便利(第13–14回で触れました)。
10.2. Production チェックリスト
-
すべての font-size は
rem(px禁止) -
clamp()の min/max はrem - Chrome・Firefox・Safari で 200%・400% ズームを確認
-
-webkit-text-size-adjust: 100%を設定 - CSS variables で type scale を構築
-
line-height は unitless(例:
1.5) - spacing(padding、margin、gap)も連動して伸縮
- 古いブラウザ用 fallback(IE11 が必要なら)
- コントラスト WCAG AA を確認
- merge 前に 200% ズーム pass — 特にセクション6の token
11. まとめと参考資料
Container Query Units について
component ベースの UI では、viewport だけが物差しではありません。タイポグラフィは カードの幅 に合わせたい — 画面全体ではなく。そのとき cqw、cqh、cqi… は vw より理にかなっています。
第13–14回 をまだ読んでいない方は、先に
container-typeをざっとどうぞ — ここは「拡張」であって総集編ではありません。
カードタイトルは viewport ではなくカードに合わせて伸縮:
.card-container {
container-type: inline-size;
}
.card-title {
font-size: clamp(1rem, 4cqw, 2rem);
}
sidebar、main、grid — 同じ component でも実際のスペースに合わせた文字サイズ。「画面が大きいか小さいか」ではなく「今いくつ分の場所があるか」。詳細は 第13–14回。第16回 で responsive パターンをまとめます。
Design Token について
fluid typography はバラバラの CSS 数行ではなく、design token の一部であるべきです:
type scale を token 化すると:
- 変数を数個直すだけでシステム全体が変わる。
- デザイナーと dev が
--font-size-2xlで話せる。「タブレットで 36 か 32px」争いが減る。 - 保守・拡張が楽になる。
結論
正しい問いは「H1 は何 px?」ではなく、「どんなルールで H1 があらゆる viewport に適応し — かつズームにも耐えるか?」。clamp() は道具にすぎません。min/max の設計、token への集約、ship 前のズームテスト — 構文よりこの3つが大事です。
いくつかのプロジェクトで使ってみて、最大のメリットは CSS の行数削減ではなく、breakpoint ごとの火消しがなくなることだと感じています。タイポグラフィが design system の一部になり、「崩れたら media query を足す」responsive typography から卒業できます。
📚 参考資料:
- MDN: clamp()
- Smashing Magazine: Modern Fluid Typography
- Smashing Magazine: Accessibility Concerns With Fluid Type
- CSS-Tricks: clamp()
- Utopia.fyi – Fluid type & space scale
- WCAG 1.4.4 Resize Text
👉 次回:
【Frontend CSS – パート16】モダン Responsive パターン — あらゆる画面サイズに対応するレイアウト設計
