1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Frontend CSS – パート15】Fluid Typography完全ガイド — clamp()・viewport単位・スケーラブルな文字サイズ設計

1
Posted at

image.png

ご注意
この記事は 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 がやってくれます:


6. 伸縮する type scale システムの構築

h1h2… ごとにバラバラに 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

typography.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)

Typography.tsx
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 なしで文字が伸縮します:

App.tsx
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 は補助にとどめ、preferredrem + 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(xs4xl + spacing) レベルが多く、desktop で伸縮がはっきり
下記rem + vw a11y 監査、200% ズームで fail rem が主、vw は軽く押すだけ

私のワークフロー:セクション6のセットで ship → ズームテスト → fail したら preferredrem + 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 は remvw にハンドルを渡さない。

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(cqwcqhcqi)は新しめ — component 単位のタイポグラフィに便利(第13–14回で触れました)。

10.2. Production チェックリスト

  • すべての font-size は rempx 禁止)
  • 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 だけが物差しではありません。タイポグラフィは カードの幅 に合わせたい — 画面全体ではなく。そのとき cqwcqhcqi… は 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 から卒業できます。


📚 参考資料:


👉 次回:

【Frontend CSS – パート16】モダン Responsive パターン — あらゆる画面サイズに対応するレイアウト設計

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?