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

広告
最近Xで投稿したline-heightの上下の余白(ハーフ・レディング)を打ち消す方法が反響を得ていましたが、こちらで紹介したcalc((1em - 1lh) / 2)はグローバルスコープのCSS変数で定義しておくと便利です。
: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などを使用して以下のような関数を定義し、ハーフ・レディングを打ち消す方法が一般的でした。
@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がサポートされていない環境を考慮する場合は@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)は行送り(レディングがベースライン)であるため、そのままデザインツールから取得した余白の値を適用するとハーフ・レディング分のズレが生じてしまいます。
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を余白の基準としてフィボナッチ数列を用いて定義したスペーシングルールをグローバルスコープで変数化したものです。
: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の文字上下余白トリミング機能はオフにするようデザイナーに掛け合ったほうが良さそうです。ご指摘ありがとうございました。
話は逸れますが、先述したデザイン庁のデザインシステムはデザインガイドラインのサンプルとして完成されているため、デザイナー・エンジニア共に一度は確認しておくことをオススメします。
line-height:1 の指定の代わりとして
僕のコーディングのポリシーとして、可能な限りline-height:1という指定は避けるというものがあります。
以前、ボタンの高さと同じ数値でline-heightを指定し、テキストを天地中央寄せする実装方法に苦言を呈したことがありますが、line-height:1を避ける理由も同様の意味合いを持っています。
デザインカンプ上でline-height:1が指定されていたとしても、将来テキストが更新されることで複数行になる可能性がありますし、テキストの変更が行われないことが期待されるテキストでも機械翻訳されることで複数行になる可能性があるため、可能な限りline-height:1という指定は避けたほうが良いという見解です。
単純に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:1として、ボタンのpaddingは1remとする」とデザインの仕様で定められている場合はpadding-blockの値をcalc(1rem + var(--leading-trim))とすることでデザインの仕様を満たしたうえで複数行の対応を行うことができます。

また、「ベースの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というプロパティを使うことでハーフ・レディングを打ち消すことが可能になるようです。
しかし、現時点では全モダンブラウザでサポートされておらず(Safari Technology Previewでのみ使用可能)、僕がこの業界に入った当時からleading-trimプロパティとして提案されていたのにも関わらず一向にサポートされる気配がなかったため、これらのプロパティが使用可能になるのは遠い未来だと予測されます。
参考リンク
ドキュメント
<length>#lh - CSS: カスケーディングスタイルシート | MDN- types:
<length>:lhunit | Can I use… Support tables for HTML5, CSS3, etc - 達成基準 1.4.8 視覚的提示
