Skip to content

Instantly share code, notes, and snippets.

@EdamAme-x
Forked from tak-dcxi/typography.md
Created January 17, 2026 10:54
Show Gist options
  • Select an option

  • Save EdamAme-x/cf8615a1aed0ce8a9a145409b158d7df to your computer and use it in GitHub Desktop.

Select an option

Save EdamAme-x/cf8615a1aed0ce8a9a145409b158d7df to your computer and use it in GitHub Desktop.
タイポグラフィCSS

タイポグラフィ

汎用的な文章の折り返し指定

  • 下のような指定を:rootに指定しておく。
:where(:root) {
  overflow-wrap: anywhere; /* 収まらない場合に折り返す */
  /* word-break: initial; 単語の分割はデフォルトに依存(初期値のため指定しなくて良い) */
  line-break: strict; /* 禁則処理を厳格に適用 */
}

text-wrap

  • テキストの行の折り返し方法を指定するプロパティ。
  • 基本的に明示する値はprettyorbalanceの 2 択で、balanceではすべての行が同じくらいの長さになるように調整されるのに対して、prettyは最後の行が一つの単語だけで終わることを防ぐ。
  • 英語では最後の行に一つだけ配置された単語を widows and orphans と呼び、テキストが読みにくくなるとして避けるべき対象とされている。そのため、:lang(en)の場合はテキスト全体にprettyを指定するのが良い。
  • 日本語では本文はベタ組みにすることが原則であるため、段落にtext-wrapの措定は行わない。見出しの引き締めには貢献できるので、text-alignstartorendならprettycenterならbalanceを指定するのが良い。
  • しかし、現在の Safari では日本語におけるそれの挙動がバグっているため、日本語の場合はtext-wrap: prettyの指定をしないほうが良い。Safari のバグが修正されたら指定することを推奨する。
:where(:is(h1, h2, h3, h4, h5, h6, p, caption):lang(en)) {
  text-wrap: pretty;
}

.-text-center {
  text-align: center;
  text-wrap: balance;
}

font-feature-settings

  • font-feature-settings: 'palt'で文字詰めができるが、日本語では本文はベタ組みにすることが原則であるためデフォルトでは指定しない。
  • 見出しに関しては文字詰めを行ったほうが可読性が向上するので指定を行う。
:where(h1, h2, h3, h4, h5, h6, caption) {
  &:lang(ja) {
    font-feature-settings: "palt";
  }
}

font-variant

  • フォントの特定のスタイルや装飾を制御することができるプロパティ。
  • 原則的にはfont-feature-settingsの上位互換であるが、使用頻度の高い'palt', 'pkna'に代わる方法が提供されていないので使い所は限られる。
  • 基本的には数字リストやローディング、料金テーブルのように数字を目立たせる箇所ではfont-variant-numeric: tabular-numsを指定して数字の幅を均等にするのに用いると良い。
.-tabular-nums {
  font-variant-numeric: tabular-nums;
}

font-kerning

  • プロポーショナルフォントの文字間隔を、隣り合う文字の組み合わせによって文字詰めすることを「カーニング」と呼ぶが、メトリクスカーニングと呼ばれる方法で、カーニングを制御するプロパティ。
  • 初期値は auto でブラウザ側に委ねられる。
  • 日本語の場合は本文はベタ組みにすることが原則であるため、font-kerning: normal の適用は可読性を悪化させる原因となる。基本的には見出しに利用し、デフォルトでは none にしておく。
  • 英語の場合はカーニングがあったほうが可読性は上げるため、デフォルトで normal を指定する。
:where(:root) {
  &:lang(en) {
    font-kerning: normal;
  }

  &:lang(ja) {
    font-kerning: none;
  }
}

:where(h1, h2, h3, h4, h5, h6, caption) {
  font-kerning: normal;
}

text-autospace

  • 日本語のなどの文字と英数字の間にスペースを入れるかどうかを制御するプロパティ。
  • Web に関しては原則的に日本語のと英数字の間にスペースを入れるのが可読性が高くなるとされており、デフォルトで text-autospace: normal を指定するのが良い。
  • ただし、pre 要素は等幅フォントでずれる、time 要素は「2025年」のような日付表記には空白が入らない、input 要素や textarea 要素のようなユーザーが入力する要素では自動挿入が入ると挙動不審になる恐れがあるため text-autospace: no-autospace を明示する。
:where(:root) {
  text-autospace: normal;
}

:where(pre, time, input:not([type="button" i], [type="submit" i], [type="reset" i]), textarea, [contenteditable]) {
  text-autospace: no-autospace;
}

text-spacing-trim

  • 約物(句読点や括弧など)とそれ以外の文字との間のスペースを制御するプロパティ。現状は Chrome のみ。
  • 通常の Web の文章は段落を「字下げ」することはなく、かつ見出しの最初の括弧の空白を消すほうが審美性に優れているためデフォルトではtext-spacing-trim: trim-startを指定する。
  • ただし、pre 要素はズレる可能性があるため、継承されないようにし、ズレないことを保証するために text-spacing-trim: space-all を明示する。
:where(:root) {
  text-spacing-trim: trim-start;
}

:where(pre) {
  text-spacing-trim: space-all;
}

text-box-trim / text-box-edge

  • ハーフレディングを除去するプロパティ
  • text-box-trim はテキストコンテンツの上端と下端のどちらを切り取るかを指定する。trim-start は上端、trim-end は下端、trim-both は両方。
  • ハーフレディングを除去する目的なら trim-both 、ハーフレディングの除去は無くていいけど横並びになった画像と見出しの上端を合わせたい…というケースでは trim-start を指定するケースが多い。
  • text-box-edge は切り取る空間の大きさを指定する。既定値は text で、上部を text-over baseline、下部を text-under baseline の位置でトリミングする。
    • 原則的には英文は cap alphabetic で上端を X の上限、下端を x の下限でトリミングするのが良い。しかし、日本語の場合は詰まりすぎる場合があるので既定値の text が良い。
    • 切り取る空間の大きさはフォントに依存する。ヒラギノフォントは text だと多少のアキが発生する。厳格に除去したいのであれば margin-block: calc((1em - 1lh) / 2) で詰めるのを推奨する。
  • line-height: 1 の代替手段としても優秀。line-height: 1 は改行した際に表示が著しく悪くなることからアンチパターンであり、デザイン上 line-height: 1 が定義されている場合でも text-box-trim を使用するべき。
  • Figma の場合はハーフレディング込みで余白が算出される(ハーフレディングを切り取る定義がされていない場合)が、 Adobe 製のツールはハーフレディングを含まずに余白が算出される。 Adobe 製のツールでカンプが作成されている場合は全称セレクタですべての要素に適用するのも選択肢。
.-trim-both {
  text-box-trim: trim-both;
  
  &:lang(en) {
    text-box-edge: cap alphabetic;
  }
}

hanging-punctuation

  • インライン軸に句読点を包括できる余白が存在するのが条件だが、hanging-punctuation: last allow-end の指定で「段落最後の閉じカッコ」と「行末の句読点」がぶら下がるようにできる。現在は Safari のみだが、そのうち Chrome にも来る予定である。
  • 行頭や行末に位置する約物文字(句読点や括弧、引用符など)を行ボックスの外側にぶら下げることは、行揃えを改善するために有効である。印刷組版では、段落の開始引用符「“」や行末の句点「。」を行の外側に出して、他の行と揃えを取る手法が用いられている。
  • 原則的には段落でのみ適用する。
  • 注意点としては、約物を行外に出すことでコンテナの論理幅からはみ出すため、場合によっては水平スクロールバーが出ることがある。インライン軸に padding とセットで指定したほうがいい。
  • 英語の場合は hanging-punctuation: first allow-end last が良さそう(参考
.-hanging {
  hanging-punctuation: last allow-end;
  
  &:lang(en) {
    hanging-punctuation: first allow-end last;
  }
}

line-clamp

  • 指定した行数でテキストを省略するプロパティ。ベンダープレフィックスなしの line-clamp は全コアブラウザでサポートされていないため、 flex のベンダープレフィックス版の指定 display:-webkit-boxflex-direction に価する -webkit-box-orient と合わせてベンダープレフィックス付きで指定する必要がある。
  • そのままだと三点リーダー込でオーバーフローするので、overflow プロパティではみ出した部分を非表示にする必要がある。この際、overflow-y: clip とすること。 先述した hanging-punctuation と組み合わせた時に overflow: hidden だと約物が切り取られ、overflow-y: hidden だとX軸の overflowauto にマッピングされてスクロールバーが表示されるためである。(※こういうイレギュラーを防ぐため、また Scroll-driven Animation や position: sticky の動きを阻害しないためにも hidden ではなく clip を優先して使ったほうがいい)
  • 汎用レイアウトとして定義しておくと良い。
.composable-line-clamp {
  display: -webkit-box;
  overflow-block: clip;
  -webkit-box-orient: block-axis;
  -webkit-line-clamp: var(--composable-line-clamp--limit, 3);

  @supports not (overflow-block: clip) {
    overflow-y: clip;
  }
}

文節区切りでの改行

  • 日本語の見出しは文節区切りでの改行と可読性がよくなる。
  • word-break: auto-phrase で文節区切りでの改行を行うことができるが、Chrome 系のみ。
  • Safari or Firefox で文節区切りでの改行を行う場合は BudouX を導入する。しかし、導入コストはあるため Chrome 系のみ文節区切りでの改行を行い、その他は従来のままというプログレッシブ・エンハンスメントの考えを持つのも選択肢。
  • 本文まで全て文節改行すると、段落内で妙な空行が生まれて可読性を悪化させるので避けること。見出し・キャプション・短文詩などは文節改行が望ましい典型例であり、そういった必要な箇所に限定して使うようにする。
:where(h1, h2, h3, h4, h5, h6, caption) {
  &:lang(ja) {
     word-break: auto-phrase;
  }
}

https://github.com/google/budoux

hyphens

  • 長い単語がすべて次の行へ送られると、その前後の行で不自然なスペースが生じてしまう。単語の途中で改行しつつ、不自然にならない音節の位置にハイフンを挿入して改行する処理のことを「ハイフネーション」と呼ぶ。
  • hyphens: auto を指定することで、自動的に単語の途中でハイフネーションを行うことができる。
  • 注意点としては hyphens: auto は言語依存のため、 lang="en" 属性などを付与して現在の言語を明示化する必要がある。
<div class="-hyphens" lang="en">
  <p>You think that's where it's at</p>
  <p>But is that where it's supposed to be?</p>
  <p>You're getting it all over me, ex-rated</p>
</div>
.-hyphens {
  hyphens: auto;
}

text-transform

  • 主にテキストを大文字表記にする text-transform: uppercase が最も使用される。
  • VoiceOver では略語として定義されているものはアルファベット毎に読み上げる(例:IT→「アイティー」、ADD→「エーディーディー」)ので、読み間違いを防ぐために依然としてこのテクニックは重要。
  • ただし、CSSで各セレクタ毎に指定すると「全て大文字」とそれ以外が入れ込んだ時に辛いのでユーティリティクラスとして定義しておく。
.-uppercase {
  text-transform: uppercase;
}

流体タイポグラフィ

  • font-size をメディアクエリ or コンテナクエリで切り分けるのは辛いので clamp() 関数を使って最小値〜最大値の範囲でフォントサイズを滑らかに変化させるようにする。
  • ただし、推奨値の計算を CSS のみで行うのも辛い。progress()関数もしくは CSS 標準の @function が広まればこのあたりは改善するが、現状では厳しい。
  • オンラインジェネレーターで算出するのは行き来するコストが掛かり、コメントを残さないと推奨値の計算手順が分からない、といった理由からなるべく避けたほうがいい。
  • Sass を使ってるなら自前の @function を作成するようにする。CSS 標準でやりたいならユーティリティクラスを作ってカスタムプロパティを受け渡しできるようにすると良い。
  • サイト全体の調和を優先するなら svi 、コンポーネント毎にスコープを切り分けるならコンテナクエリ + cqi のように相対先の単位を出し分けできるようにしておく。
.-fluid-text {
  --_u-min-width: var(--fluid-text--min-width, 375);
  --_u-max-width: var(--fluid-text--max-width, 1280);
  --_u-min-font-size: var(--fluid-text--min-font-size, 14);
  --_u-max-font-size: var(--fluid-text--max-font-size, 16);
  --_u-base-font-size: var(--fluid-text--base-font-size, 16);
  --_u-relative-unit: var(
    --fluid-text--relative-unit,
    100svi
  ); /* 100svi or 100cqi */

  --_u-slope: calc(
    (var(--_u-max-font-size) - var(--_u-min-font-size)) /
      (var(--_u-max-width) - var(--_u-min-width))
  );
  --_u-intercept: calc(
    var(--_u-min-font-size) - var(--_u-slope) * var(--_u-min-width)
  );
  --_u-font-size: clamp(
    var(--_u-min-font-size) / var(--_u-base-font-size) * 1rem,
    var(--_u-slope) * var(--_u-relative-unit) + var(--_u-intercept) /
      var(--_u-base-font-size) * 1rem,
    var(--_u-max-font-size) / var(--_u-base-font-size) * 1rem
  );

  font-size: var(--_u-font-size);
}

--_u- 始まりなのはコンポーネントのローカルカスタムプロパティ(例:--_min-font-size)とのバッティングを防ぐため。

px vs rem

  • 今までは rem 指定が有効に働くケースは Chrome の文字拡大機能(not ズーム機能)のみであり、「誰が使ってるか分からないマイナーな機能をサポートするコストが見合わない」「Chrome が px でも文字拡大できるようにすればいい」といった意見にも賛同できるところがあったため「どっちでもいい」というスタンスを取ってきたが、<meta name="text-scale" /> の登場によって考えが変わった。(参考
  • text-scale が有効な場合、OS の文字スケール設定とブラウザの文字スケール設定の両方に比例してフォントサイズが拡大・縮小される。すべてのデバイスにおいて OS レベルの文字スケール設定を尊重する簡単な方法は存在しないから、rem とか em 使って実装してるサイトであればそれを OS レベルの文字スケール設定として再定義すればいいのでは?って感じらしい。px では動作しないことが(Draft段階であるものの)仕様にも書かれている
  • 単純に「rem が常に正解」「px はダメ」というような二者択一ではない。全てを rem で定義するのはズーム機能との棲み分けができていない。
  • px or rem は「この値はユーザーがデフォルトフォントサイズを大きくしたとき、一緒に大きくなるべきか?」 を使い分けの基準にすべき。
    • テキストサイズ、段落の垂直マージン、行間などはフォントサイズと連動させた方がいいので rem などを使う。
    • 一部装飾的な境界線幅、細かいデザインディテールなどは文字拡大に伴って大きくなっても嬉しくないので px を使う。
    • テキストを含むコンテンツの幅やメディアクエリやコンテナクエリのブレイクポイントは文字拡大時に窮屈になったりレイアウトが崩壊する可能性があるから rem などを使った方がいい。
    • 水平方向の padding は文字サイズと連動すると一行あたりの文字数が減り、可読性を落とす可能性があるので px を検討する。
  • 重要なことはremを使う以上ブラウザの文字拡大機能を有効にして検証すべきだということ。アクセシビリティ重視のために rem を使いましょうと言いつつ、 :rootfont-size: 10px などの固定値を指定する実装をたまに見かけるが、文字拡大機能を有効にして検証すれば動いていないことは明白であるはず。「font-size には rem を使う」というルールが独り歩きして肝心なことを見落としている。

  • 余談であるが、メディアクエリやコンテナクエリのブレイクポイントには calc()max() などの数学関数が利用できる。
  • ブレイクポイントを remem に変換する場合は @media (width >= calc(768 / 16 * 1em)) のように指定したほうが計算機を使う手間が省け、元のピクセル値がわかりやすくなるメリットがあるので推奨する。
    • メディアクエリはビューポートを基準とするため、メディアクエリの中ではブラウザの文字サイズを基準に計算される。そのため、remem もフォントサイズの変更が行われていない場合は 16px 相当になる。
    • コンテナクエリの場合は container-type: inline-size を指定した要素のフォントサイズを基準として計算される。

手動改行

  • 各見出しにおいて手動で改行を行う場合は、ポエムなどの文章構造的に意味のある改行以外では <br> は使わずに CSS で改行制御を行う。
  • 原則的にはdisplay: inline flow-rootを指定したspan要素か、親要素にdisplay: block flex + flex-wrap: wrapを指定するようにする。
  • 手動改行は多くのケースでは日本語都合であるため、:lang(ja) の時のみ上記のアプローチを行い、その他の言語では「<span>そのものを無いもの」とする方針にする。
.-br {
  display: contents;

  &:lang(ja) {
    display: block flow;
  }
}

.-wbr {
  display: contents;

  &:lang(ja) {
    display: inline flow-root;
  }
}

分離禁止の明示

文章によっては改行によって分割されると不都合なワードも存在する。

分割させたくない文字と文字の間には「‍(zeroゼロ width幅 joiner接合子)」を挿入することで分離禁止な単語として明示することができる。

<p>marginが相&zwj;殺するのを防止するために、なるべくmarginの向きは上方向に統一しておきます。</p>

上記のケースでは「相殺する」の「殺」が頭に来ると不穏な言葉が生まれてしまうので、それを防止している。

結論 

:where(:root) {
  text-spacing-trim: trim-start;
  text-autospace: normal;
  line-break: strict;
  overflow-wrap: anywhere;
  
  &:lang(ja) {
    font-kerning: none;
  }

  &:lang(en) {
    font-kerning: normal;
  }
}

:where(:is(h1, h2, h3, h4, h5, h6, p, caption):lang(en)) {
  text-wrap: pretty;
}

:where(h1, h2, h3, h4, h5, h6, caption) {
  font-kerning: normal;

  &:lang(ja) {
    font-feature-settings: "palt";
    word-break: auto-phrase;
  }
}

:where(pre) {
  text-spacing-trim: space-all;
}

:where(pre, time, input:not([type="button" i], [type="submit" i], [type="reset" i]), textarea, [contenteditable]) {
  text-autospace: no-autospace;
}

.-text-center {
  text-align: center;
  text-wrap: balance;
}

.-trim-both {
  text-box-trim: trim-both;
  
  &:lang(en) {
    text-box-edge: cap alphabetic;
  }
}

.-hanging {
  hanging-punctuation: last allow-end;
  
  &:lang(en) {
    hanging-punctuation: first allow-end last;
  }
}

.-uppercase {
  text-transform: uppercase;
}

.-hyphens {
  hyphens: auto;
}

.-tabular-nums {
  font-variant-numeric: tabular-nums;
}

.-br {
  display: contents;

  &:lang(ja) {
    display: block flow;
  }
}

.-wbr {
  display: contents;

  &:lang(ja) {
    display: inline flow-root;
  }
}

.-fluid-text {
  --_u-min-width: var(--fluid-text--min-width, 375);
  --_u-max-width: var(--fluid-text--max-width, 1280);
  --_u-min-font-size: var(--fluid-text--min-font-size, 14);
  --_u-max-font-size: var(--fluid-text--max-font-size, 16);
  --_u-base-font-size: var(--fluid-text--base-font-size, 16);
  --_u-relative-unit: var(
    --fluid-text--relative-unit,
    100svi
  ); /* 100svi or 100cqi */

  --_u-slope: calc(
    (var(--_u-max-font-size) - var(--_u-min-font-size)) /
      (var(--_u-max-width) - var(--_u-min-width))
  );
  --_u-intercept: calc(
    var(--_u-min-font-size) - var(--_u-slope) * var(--_u-min-width)
  );
  --_u-font-size: clamp(
    var(--_u-min-font-size) / var(--_u-base-font-size) * 1rem,
    var(--_u-slope) * var(--_u-relative-unit) + var(--_u-intercept) /
      var(--_u-base-font-size) * 1rem,
    var(--_u-max-font-size) / var(--_u-base-font-size) * 1rem
  );

  font-size: var(--_u-font-size);
}

.composable-line-clamp {
  display: -webkit-box;
  overflow-block: clip;
  -webkit-box-orient: block-axis;
  -webkit-line-clamp: var(--composable-line-clamp--limit, 3);

  @supports not (overflow-block: clip) {
    overflow-y: clip;
  }
}

※kiso.cssを使用している場合はいくつかの指定が不要になる。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment