CSS

line-heightのハーフ・レディングを打ち消す`calc((1em - 1lh) / 2)`をCSS変数に定義しておくとよい

この記事は2024年5月時点の内容です。記事の最後で「遠い未来」と予測したtext-box-trimとtext-box-edgeは、Chrome 133、Safari 18.2(iOS 18.2)、Firefox 154でサポートされ、2026年8月に全てのモダンブラウザで使えるようになりました。なお、2026年11月時点のFirefoxはtext-box-edgeのcapやalphabeticといった値をまだ描画に反映しないため、初期値のautoと同じ位置で刈られます。現在の当ブログでは、和文はtext-box-trim: trim-bothだけを指定し、欧文はtext-box-edge: cap alphabeticも併せて指定することでハーフ・レディングを打ち消しています。

ただし、text-box-trimはflexやgridのコンテナには効かないため、calc((1em - 1lh) / 2)を使う場面は今も残っています。また、Tailwind CSSの例はv3の設定ファイルの書き方です。

最近Xで投稿したline-heightの上下の余白(ハーフ・レディング)を打ち消す方法が反響を得ていましたが、こちらで紹介したcalc((1em - 1lh) / 2)はグローバルスコープのCSS変数で定義しておくと便利です。

参考:最近投稿したポスト

margin-block: calc((1em - 1lh) / 2); https://t.co/SDV56HSoVe

ポストを別枠で表示する

🙆‍♂ Recommended
:root {
	--leading-trim: calc((1em - 1lh) / 2);
}

calc((1em - 1lh) / 2) とは

現在のフォントサイズ(1em)から行の高さ(1lh)を引き、その差を2で割ることでハーフ・レディングを取り除いた値を算出する計算式です。

例えばp要素に以下のような指定を行ったとします。

p {
	font-size: 1rem;
	line-height: 1.5;
	margin-block: calc((1em - 1lh) / 2);
}

lhという単位に見慣れない方もいるかと思われますが、これは現在のline-heightと同じ長さを表す新しく登場した単位です。この例ではline-heightはフォントサイズの1.5倍なので、もし1remが16pxであれば1lhは24pxとなります。

この場合、行の高さと文字の高さの負の差は1em - 1lh、つまり16px - 24pxで-8pxです。それを片方の値を算出するために2で割ると-4pxになります。したがって、margin-block: calc((1em - 1lh) / 2)は、書式のブロック方向(横書き時:上下)にそれぞれハーフ・レディングの大きさ(今回では4px)分のネガティブマージンを設定するということになります。

従来の上下の余白を打ち消す方法との比較

lhが登場するまではSassの@mixinなどを使用して以下のような関数を定義し、ハーフ・レディングを打ち消す方法が一般的でした。

Sassの関数の例
@mixin leadingTrim($lh) {
	&::before,
	&::after {
		content: '';
		display: block flow;
		inline-size: 0;
		block-size: 1px; // marginが貫通しないようにする
	}

	// 前の要素との相殺を防ぐために::beforeに`margin-block-end`、::afterに`margin-block-start`を指定する
	&::before {
		margin-block-end: calc((1 - #{$lh}) * 0.5em);
	}

	&::after {
		margin-block-start: calc((1 - #{$lh}) * 0.5em);
	}
}

p {
	font-size: 1rem;
	line-height: 1.5;
	@include leadingTrim(1.5); //line-heightで指定されている値を指定する
}

この方法はその要素が持つline-heightの値を事前に知っておく必要があり、レスポンシブデザインなどでline-heightの値が変更された場合に再度計算を見直す必要があるため柔軟性や保守性に欠けていました。

一方、lhを使用することで現在のline-heightに基づいた相対的なサイズを指定できるため、ハーフ・レディングを直接的かつ動的に打ち消すことができます。これにより、要素が持つline-heightの値を知らなくても上下の余白を打ち消すことができ、またline-heightの値が変更された場合にも手動で再計算を行う必要がなくなるため柔軟性や保守性が向上します。

lh は全モダンブラウザで対応済み

lhは現在全てのモダンブラウザで対応されています。ただし、iOS Safari では 16.4 からの対応となるため、iOS 16 以上をサポートする場合はフォールバックを検討する必要があります。

lh unit

広く利用可能 2026年5月から(新しく利用可能になったのは2023年11月)

執筆時点(2024年5月)は「新しく利用可能」でした

109
109
120
16.4
16.4

web-features 3.44.0 のデータ

webstatus.dev で見る

lhがサポートされていない環境を考慮する場合は@supports機能クエリを使用して0pxを定義しておくと良いかもしれません。

フォールバックの例
:root {
	--leading-trim: calc((1em - 1lh) / 2);
}

@supports not (top: 1lh) {
	:root {
		--leading-trim: 0px; /* `px`などの単位が必要 */
	}
}

このフォールバックをすることでmargin-block:calc(24px + var(--leading-trim))のような指定を行った場合、ハーフ・レディングを含んだ値にはなるものの計算エラーによって余白が無くなるという事態は防ぐことができます。

calc((1em - 1lh) / 2) をCSS変数に定義しておいたほうが良い理由

calc((1em - 1lh) / 2)をグローバルスコープのCSS変数で定義しておくとよい理由は次のとおりです。

Adobe製デザインツールを元にコーディングする際の余白調整として

現在デザインツールとして主流のFigmaはハーフ・レディングに対応しているものの、Adobe製のデザインツール(XD、Illustrator、Photoshop)は行送り(レディングがベースライン)であるため、そのままデザインツールから取得した余白の値を適用するとハーフ・レディング分のズレが生じてしまいます。

参考:せきゆおうさんのポスト

FigmaとAdobe XDでは line-heightの扱いが違うと聞いて検証した。

結論、デザインカンプの数値通りにコーディングすると、Figmaとブラウザ(Chrome)の表示はほぼ同じでAdobe XDは少しズレた。

以下の比較はそれぞれのスクリーンショットです。

検証環境:macOS Monterey
検証日:2022年9月21日 pic.twitter.com/mNVpFieigZ

ポストを別枠で表示する

Xでは定期的にピクセルパーフェクトは必要か否か(新しいタブで開く)で盛り上がっていますが、大体このハーフ・レディング分のズレが要因になっているように思われます。個人的な見解ですが、フォントサイズが16pxでline-heightが1.5の場合、そのまま余白を設定してしまうとその要素だけで8px分のズレが生じるためピクセルパーフェクト云々の範疇に収めるべきではないと感じます。

ちなみにピクセルパーフェクトの議論においてマージンが40pxズレているとか、フォントや配色が間違っているなどをまとめて「ピクセルパーフェクト」と言っている方も散見されますが、それは単にデザインを再現していないだけなのでピクセルパーフェクトと一緒くたにするのは誤りです。

このため、Adobe製デザインツールを元にコーディングする場合はその余白にハーフ・レディングが絡むかどうかを確認し、calc()関数と併せてCSS変数で用意しておいたcalc((1em - 1lh) / 2)を呼び出すことを推奨します。

h2 {
	/* デザインカンプで取得した余白が32pxの場合 */
	margin-block-start: calc(32px + var(--leading-trim));
}

上記の例のようにcalc(32px + var(--leading-trim))とすることで「デザインカンプで取得した余白(32px)にleading-trimを加える」と明示でき、コードの可読性が向上します。

もしデザインガイドラインで余白がトークン化されている場合は--spacing-lg-trimのようにハーフ・レディングを除いた余白も変数化しておくと良いでしょう。

次の例はデジタル庁のデザインシステム(新しいタブで開く)で用いられている、8pxを余白の基準としてフィボナッチ数列を用いて定義したスペーシングルールをグローバルスコープで変数化したものです。

余白のCSS変数の例
:root {
	--leading-trim: calc((1em - 1lh) / 2);
	--spacing-unit: 0.5rem; /* 8px */

	--spacing-xs: calc(var(--spacing-unit) / 2);
	--spacing-sm: var(--spacing-unit);
	--spacing-md: calc(var(--spacing-unit) * 2);
	--spacing-lg: calc(var(--spacing-unit) * 3);
	--spacing-xl: calc(var(--spacing-unit) * 5);
	--spacing-2xl: calc(var(--spacing-unit) * 8);
	--spacing-3xl: calc(var(--spacing-unit) * 13);

	--spacing-xs-trim: calc(var(--spacing-xs) + var(--leading-trim));
	--spacing-sm-trim: calc(var(--spacing-sm) + var(--leading-trim));
	--spacing-md-trim: calc(var(--spacing-md) + var(--leading-trim));
	--spacing-lg-trim: calc(var(--spacing-lg) + var(--leading-trim));
	--spacing-xl-trim: calc(var(--spacing-xl) + var(--leading-trim));
	--spacing-2xl-trim: calc(var(--spacing-2xl) + var(--leading-trim));
	--spacing-3xl-trim: calc(var(--spacing-3xl) + var(--leading-trim));
}

Tailwind CSSをご利用の場合はbaseレイヤーに--leading-trim: calc((1em - 1lh) / 2)を指定したCSSファイルを用意し、theme.spacingで同様の設定を行っておくと良いでしょう。

const SPACING_UNIT = '0.5rem'

module.exports = {
	theme: {
		spacing: {
			xs: `calc(${SPACING_UNIT} / 2)`,
			sm: `${SPACING_UNIT}`,
			md: `calc(${SPACING_UNIT} * 2)`,
			lg: `calc(${SPACING_UNIT} * 3)`,
			xl: `calc(${SPACING_UNIT} * 5)`,
			'2xl': `calc(${SPACING_UNIT} * 8)`,
			'3xl': `calc(${SPACING_UNIT} * 13)`,
			'xs-trim': `calc(${SPACING_UNIT} / 2 + var(--leading-trim))`,
			'sm-trim': `calc(${SPACING_UNIT} + var(--leading-trim))`,
			'md-trim': `calc(${SPACING_UNIT} * 2 + var(--leading-trim))`,
			'lg-trim': `calc(${SPACING_UNIT} * 3 + var(--leading-trim))`,
			'xl-trim': `calc(${SPACING_UNIT} * 5 + var(--leading-trim))`,
			'2xl-trim': `calc(${SPACING_UNIT} * 8 + var(--leading-trim))`,
			'3xl-trim': `calc(${SPACING_UNIT} * 13 + var(--leading-trim))`,
			trim: 'var(--leading-trim)',
		},
	},
}

CSS変数をbaseレイヤーに置いておくことでarbitrary valuesで呼び出すことができるため、利便性が増します。

<a href="{href}" class="plb-[calc(1.5em_+_var(--leading-trim))] pli-[1em] ...">ボタンテキスト</a>

コードの例で使用しているplbおよびpliはtailwindcss-logicalで用意されているpadding-block padding-inlineのユーティリティクラスです。

また、Figmaを使用している場合においても文字上下余白トリミング機能(vertical-trim)が一部では有効になっていたり、意図せず全体的に有効になっている場合も考えられるためデザインツールの種類にかかわらずcalc((1em - 1lh) / 2)をCSS変数に定義しておくことを推奨します。

Figmaの文字上下余白トリミングはフォントのbase lineからcap lineまでの高さにトリミングする機能であるため、現状のCSSでは再現が厳しいという指摘を頂きました。そのため、Figmaの文字上下余白トリミング機能はオフにするようデザイナーに掛け合ったほうが良さそうです。ご指摘ありがとうございました。

TAKさん、すみません。細かいところなのですが、Figmaの文字上下余白トリミングは、フォントのbase lineからcap lineまでの高さ(Cap height)にトリミングする機能なので、calc((1em - 1lh) / 2)とは微妙に異なり、現状のCSSでは完全に再現することができない、という認識です。

ポストを別枠で表示する


話は逸れますが、先述したデジタル庁のデザインシステム(新しいタブで開く)はデザインガイドラインのサンプルとして完成されているため、デザイナー・エンジニア共に一度は確認しておくことをオススメします。

デジタル庁のデザインシステム見る度に感動する。全人類見た方がいい。 https://t.co/Gm1u3Wdtps

ポストを別枠で表示する

line-height:1 の指定の代わりとして

僕のコーディングのポリシーとして、可能な限りline-height:1という指定は避けるというものがあります。

デザインカンプではline-height:1な要素もテキストの変更や機械翻訳で複数行になる可能性あるのでなるべくこの指定をした方が良いかと思います… https://t.co/RiWs7pwfUU

ポストを別枠で表示する

以前、ボタンの高さと同じ数値でline-heightを指定し、テキストを天地中央寄せする実装方法に苦言を呈したことがありますが、line-height:1を避ける理由も同様の意味合いを持っています。

デザインカンプ上でline-height:1が指定されていたとしても、将来テキストが更新されることで複数行になる可能性がありますし、テキストの変更が行われないことが期待されるテキストでも機械翻訳されることで複数行になる可能性があるため、可能な限りline-height:1という指定は避けたほうが良いという見解です。

参考:びしーさんのポスト

「ここの文言は固定だから2行になんかならないやーい」って思ってるひと!
外国人の人がブラウザの翻訳機能使ったらどうなると思いますか!?
こうですよ!(バァン pic.twitter.com/TQByJe0HBN

ポストを別枠で表示する

単純にline-height:1は読みにくいというのもありますが、特に認知障害を持つ方は行間が狭すぎると文字を追うのが難しくなる可能性があり、line-height:1は複数行になった際にアクセシビリティの観点で問題となる可能性があります。可能な限りデザイナーにline-height:1が指定されている部分は複数行になった際にどれくらいの行間を設けるか?は確認したほうが良いですが、「WCAG(Web Content Accessibility Guideline) 2.1(新しいタブで開く)」では達成基準の一つ「1.4.8 視覚的提示」で「段落中の行送りは、少なくとも1.5文字分ある。そして、段落の間隔は、その行送りの少なくとも1.5倍以上ある」と記載されているため、特に指定がなければ最低でもline-height:1.5程度を確保することが望ましいという見解です。

ウェブデザインにおけるline-heightについて

ウェブデザインにおけるline-heightってけっこう曲者で、CSSを理解してデザインしないと「空き」の設計が破綻したりコーディングで苦労することになります。FigmaやAdobe XD、Affinity Design […]

parashuto.com

例えば「ボタンのラベルはline-height:1として、ボタンのpaddingは1remとする」とデザインの仕様で定められている場合はpadding-blockの値をcalc(1rem + var(--leading-trim))とすることでデザインの仕様を満たしたうえで複数行の対応を行うことができます。

開発者ツールのスタイル欄で、padding-blockのcalc(1.5em + var(--leading-trim))を赤枠で囲んだもの
参考:当ブログの汎用的なボタンコンポーネントのpaddingの指定

また、「ベースのline-heightは1にしておくとコーディングしやすい」という技術発信を見かけましたが、これも上記の理由から避けたほうが良いでしょう。

英文のハーフ・レディングを打ち消すなら calc((1cap - 1lh) / 2) がおすすめ

英文でハーフ・レディングを打ち消す場合、calc((1em - 1lh) / 2)では少しスペースが余ることがあります。英文のハーフ・レディングを打ち消したい場合はcalc((1cap - 1lh) / 2)の使用も検討してみてください。cap は要素のフォントにおける「cap height」(通常の大文字の高さ)を表します。

:root {
	--leading-trim: calc((1em - 1lh) / 2);
}

:lang(en) {
	--leading-trim: calc((1cap - 1lh) / 2);
}

上記のように:lang(en)擬似クラスで--leading-trimの値を書き換えることで、lang="en"属性が指定された文章はcapを参照してハーフ・レディングを取り除いた値を算出するようになります。

汎用性を持たせるならユーティリティクラスを用意するのもよい

指定する要素で疑似要素が使用されていないことが条件となりますが、従来の方法で挙げたアプローチでハーフ・レディングを打ち消すことも可能です。ユーティリティクラスとして扱えばclassを付与するだけでハーフ・レディングを打ち消すことが可能となります。

ユーティリティクラスの例
.leading-trim {
	&::before,
	&::after {
		content: '';
		display: block flow;
		inline-size: 0;
		block-size: 1px;
	}

	&::before {
		margin-block-end: var(--leading-trim);
	}

	&::after {
		margin-block-start: var(--leading-trim);
	}
}

Tailwindを使用している場合はaddUtilitiesでユーティリティクラスを追加するのも良いでしょう。

将来 text-box-trim & text-box-edge プロパティが全モダンブラウザで使用可能になればこの指定は不要になる

将来的にはtext-box-edge:textとtext-box-trim:bothというプロパティを使うことでハーフ・レディングを打ち消すことが可能になるようです。

text-box

一部のブラウザのみ

執筆時点(2024年5月)から変わっていません

133
133
未対応
18.2
18.2

web-features 3.44.0 のデータ

webstatus.dev で見る

しかし、現時点では全モダンブラウザでサポートされておらず(Safari Technology Previewでのみ使用可能)、僕がこの業界に入った当時からleading-trimプロパティとして提案されていたのにも関わらず一向にサポートされる気配がなかったため、これらのプロパティが使用可能になるのは遠い未来だと予測されます。

参考リンク

ドキュメント

書籍

参考記事