📍

HTML/CSS - display:flex も子要素も使えない制約で、要素内のテキストを縦方向の中央に配置する方法

2024/11/01に公開

はじめに

先日、とあるサンプル Web アプリの作成中に、display:flex もレイアウト調整用の子要素も使えない制約の下、要素内のテキストを縦方向の中央に配置する必要に迫られました。とりあえず四苦八苦しながら実装はできました。これが本当に最適解なのかどうか確信ないのですが、その点も含めて共有します。

要件

以下のようなスプリッターコンテナコンポーネントのサンプルプログラムを作っておりました。

ここで、以下のようないくつかの要件・こだわりがありました。

  • ペイン幅が狭められてペイン内のテキストが見切れるときは、「...」の省略記号表示を行なう (text-overflow: ellipsis)。
  • サンプルプログラムなので、マークアップを複雑にしたくない。なので、ペイン内の要素は <h1>{ここにテキスト}</h1> 要素のみ。 レイアウトのためだけの要素は追加しない。CSS だけで頑張る (その代わり CSS は多少ややこしくなっても構わない)。
  • 詳細割愛するが諸事情あって、枠線はペイン内の <h1> 要素に付ける (ペインを構成する要素には付けない)。

課題を簡略化すると、まず、以下のような HTML があったとして、

<div class="pane">
  <h1>Hello, World!</h1>
</div>

以下のような CSS を適用します。

.pane {
  /* 例えば、ペインのサイズは縦横 300px */
  width: 300px;
  height: 300px;
  position: relative;
}

h1 {
  /* 枠線を引く */
  border: dashed 2px blueviolet;

  /* 親要素 = ペイン要素の内側めいっぱいに広げる */
  position: absolute;
  inset: 0;

  /* マージンは 0 にリセットし、見栄えのために左右の 20px のパディングを付ける */
  margin: 0;
  padding: 0 20px;

  /* 見栄えのために、テキストは左右中央寄せ */
  text-align: center;

  /* 要素内のテキストは折り返しを禁じつつ、見切れたところは省略記号表示とする */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

ここまでの段階で以下のような見た目が得られます。

試しに、CSS の定義で、.panewidth300px から 200px に変更すると、以下のように、<h1> 要素内で見切れたテキストは「...」による省略記号での表示となることが確認できます。

ここまではいいのですが、さらに、<h1> 要素内のテキストを、上下方向の中央に配置しようとして行き詰まりました。

display: flex を使うと「...」が表示されない!?

上下方向の中央寄せといえば、自分の中では display: flex の出番です。ということで、<h1> 要素に対する CSS のスタイル定義に、以下のように書き加えてみました。

...
h1 {
  ...
  /* 以下のスタイル指定を追加 */
  display: flex;
  align-items: center;
  justify-content: center;
}

表示を確認してみると、良い感じで上下左右中央寄せでテキストが表示されているようです。

しかしここで親要素の幅を 200px に狭めてみると...

残念なことに、見切れたテキストの「...」による省略記号表示がされません。 正直なところ、自分は理解が浅くよくわかっていないのですが、display: flex をこのように適用すると、text-overflow: ellipsis が効かなくなる のでしょうか。

まぁ、以下の .container 要素のように、さらにレイアウト用の要素を追加したりすれば、解決できそうなのですが、

<div class="pane">
  <div class="container"> <!-- 👈こういうレイアウト調整用の要素を追加する -->
    <h1>Hello, World!</h1>
  </div>
</div>

しかしながら、前述のこだわり・要件のため、ペイン内には <h1> 要素しか配置したくありません。

ということで、display: flex なし・レイアウト用の要素追加もなし、で、引き続き CSS のみでこのシナリオ・要件を実現できるものなのか、暫し試行錯誤を繰り返しました。

::before で、高さ 50% の疑似要素を足してみた

いろいろひねくり回して行き着いた自分の解法は、「::before で、高さ 50% の疑似要素を足す」 という方法でした。つまり、この疑似要素によってテキストが下に押しやられ、結果としてテキストが上下方向の中央に表示される、という方法です。

具体的には、CSS スタイル定義に、以下のように、h1::before セレクタとその CSS スタイル定義を追加しました。ただし、本当に「高さ 50%」の要素を挟むだけにしてしまうと、テキストそれ自体の高さ分、下に行きすぎてしまいます。そのため「0.8em」というマジックナンバーで少し高さを差し引いて調整しています。

...
h1::before {
  content: "";
  display: block;
  height: calc(50% - 0.8em);
}

実行結果は以下のとおりです。

これでテキストは上下左右中央寄せで表示されつつ、親要素の幅が狭くテキストが見きれるときは「...」による省略記号が表示されるようにできました。

[2024/11/04 追記] h1 要素への line-hight 指定でできた!

テキストの上下中央配置といえば、もうひとつ、line-height による指定方法があります。すなわち、ある要素内のテキストは、line-height で指定された行の高さに対し上下中央に配置されます。ですので、今回の例で言えば、<h1> 要素に対する line-height による行の高さ指定を追加し、その値として、.pane 要素の高さを指定できればよいことになります。具体的には以下のような CSS になります。

.pane {
  ...
  height: 300px;
}

h1 {
  border: dashed 2px blueviolet;
  ...
  /* 👇 .pane と同じ高さを h1 の line-height に指定する
        (厳密には上下の枠線の太さぶん (上下それぞれ 2px、計 4px) を差し引く) */
  line-height: calc(300px - 2px - 2px);
}

しかしながら、これは冒頭の前提条件で明言できていなかったのですが、.pane の高さは、JavaScript コードによって、実行時に動的に変更されるのです (なにせ、スプリッターコンテナコンポーネントなので)。そのため、上記実装例のように、line-height: calc(300px ...) のような固定値を記述することでは対処できません。さりとて、line-height100% を指定しても無駄です。というのも、line-height に対するパーセント値指定は、"親要素の高さに対する割合" ではなく、"要素自身のフォントサイズに対する割合"になるからです。

そこで登場するのが、「コンテナクエリ単位」 というサイズ単位です。詳しい説明は MDN の解説ページに譲りますが、これは、"コンテナである" と CSS で明示された直近の要素に対する割合 (パーセンテージ) で指定する単位で、高さであれば cqh (Container Query Height) で指定します。このコンテナクエリ単位、やや新しめの機能なのですが、2022年9月頃には Chromium 系のブラウザでサポートされ、2023年10月に Firefox がサポートしたことで概ねメジャーなブラウザでは利用可能な機能となりました (下記、"Can I use" 参照)。

https://caniuse.com/css-container-query-units

実際にやってみましょう。<h1> 要素の line-height.pane 要素の高さに合わせたいので、まずは、.pane 要素を「この要素はコンテナである」と CSS で明示します。

.pane {
  ...
  container-type: size; /* 👈これを指定 */
}
...

あとは、このコンテナの子孫要素である <h1> 要素について、line-height100cqh を指定すれば、直近の親コンテナである .pane 要素の高さが指定できます (厳密には、上下の枠線の太さぶんを差し引く必要があります)。

...
h1 {
  border: dashed 2px blueviolet;
  ...
  /* 👇 コンテナクエリ単位で行の高さを指定! */
  line-height: calc(100cqh - 2px - 2px);
}

これで、.pane 要素の高さがいかように変更されても、常に <h1> 要素の行の高さは .pane 要素の高さに一致し、<h1> 要素内のテキストが上下方向中央に配置されるようになりました! 本記事の初回投稿時点で自分がやっていたような、変な疑似要素の追加やマジックナンバーに基づく調整は一切不要で、とても簡潔に実装できました。

コード全体を確認するには、以下の CodePen に配置してありますので、そちらをご確認ください。

[2024/11/04 改訂] おわりに

本記事の最初の投稿以降、𝕏 上でいろいろ助言頂き、line-height とコンテナクエリ単位の組み合わせですっきりした解決方法に至ることができました。詳しい顛末は以下の 𝕏 への投稿とそのリプを参照ください。

https://x.com/jsakamoto/status/1852317212373397794

また、この 𝕏 でのリプを通して、どうして flex を使った場合にテキストの省略表示が適用されないかも教えて頂きました。display: flex が適用された要素については、その要素内のテキストに対して無名ボックスが生成されるため、なのですね。なので、そうして生成された無名のボックスノードに対して text-overflow: ellipsis みたいな指定が可能であればまた実現の道筋も違ったかもですが、実際のところ、CSS はそうはできていないということですね。

反応頂いた各位、どうもありがとうございました。


ということで、普通なら display: flex でおしまい、のところ、こだわり・要件があったために苦労しつつ、どうにか解決策を捻り出した、という話でした。

とはいえ、疑似要素の高さ指定で「- 0.8em」という恣意的な値が出てきたり、手放しで歓迎できる方法ではないですね。自分はうまく解決できませんでしたが、もしもう少し上手くやることで display: flextext-overflow: ellipsis が併用できるのであればそれに越したこともないですし。

何はともあれ、今時点では、もっと他にマシな方法は自分には思いつかなかったので、ひとまず以上を共有とします。実装例を以下の CodePen に配置しておきますので、いろいろ試されたい方はぜひ、もっと良い方法がないかいじってみてください。

Discussion