CSSのinterpolate-sizeで高さautoを動かす方法

​​CSS​JavaScript,Playwright,アクセシビリティ

アコーディオンや折りたたみメニュー、「続きを読む」ボタンのように、高さを 0 や決まった値から auto へ広げる開閉は、interpolate-size: allow-keywords を1行足すと CSS の transition だけで動きます。ただし、MDN の互換性データ(@mdn/browser-compat-data 8.1.9)では、正式版で対応しているのは Chrome と Edge などの Chromium 系だけです。以下では、ボタンで開閉するパネルを例に、書く場所と効かない組み合わせ、対応していないブラウザ向けの書き分けを、測った高さとあわせて紹介します。

補足

検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。HTML と CSS のページを Playwright で開いてボタンを押し、描画のコマごとに要素の高さを測りました。ブラウザの対応バージョンは @mdn/browser-compat-data 8.1.9 の値です。Firefox と Safari、スマートフォンの実機は、この環境で動かせないため試していません。対応していないブラウザでの動きは、Chromium に存在しないプロパティ名や関数名をわざと書いて確かめています。

ボタンで開閉するパネルを、height: 0 から auto へ動かす

HTML は、ボタンとパネルを並べただけです。ボタンを押すと、JavaScript でパネルに is-open クラスを付け外しします。

html
<button class="toggle" type="button" aria-expanded="false" aria-controls="p1">お知らせを見る</button>
<div class="panel" id="p1">
  <div class="inner">
    <p>11月の営業日は、平日の10時から18時までです。<a href="#">営業日の一覧</a></p>
  </div>
</div>
js
document.querySelectorAll('.toggle').forEach((button) => {
  button.addEventListener('click', () => {
    const panel = document.getElementById(button.getAttribute('aria-controls'));
    const open = panel.classList.toggle('is-open');
    button.setAttribute('aria-expanded', String(open));
  });
});

CSS では、閉じたときの height: 0 と開いたときの height: auto を transition でつなぎます。

css
/* ボタンで開閉するパネル:height を 0 から auto へ動かす */
:root {
  interpolate-size: allow-keywords;
}
.panel {
  height: 0;
  overflow: hidden;
  visibility: hidden;
  transition:
    height 300ms ease,
    visibility 300ms;
}
.panel.is-open {
  height: auto;
  visibility: visible;
}
.panel p {
  margin: 0;
  padding: 8px 0 12px;
}
@media (prefers-reduced-motion: reduce) {
  .panel {
    transition: none;
  }
}

MDN の interpolate-size のページによると、既定の値は numeric-only で、auto のような中身で決まる大きさ(内在サイズ)へは補間しません。allow-keywords にすると、0 のような長さと auto のあいだを補間できるようになります。

ボタンを押してから .panel の高さを測りました。50ms ごとの値は、その時刻を過ぎて最初に描画されたコマの値です。

text
[パネル、:root に allow-keywords] 最初の高さ 0px
  開く: 0ms 0px / 50ms 23px / 100ms 47px / 150ms 60px / 200ms 67px / 250ms 71px / 300ms 71px / 350ms 71px
  閉じる: 0ms 71px / 50ms 47px / 100ms 24px / 150ms 11px / 200ms 4px / 250ms 1px / 300ms 0px / 350ms 0px

:root の1行を既定の numeric-only に戻すと、押した直後のコマで開ききり、閉じるときも一度に 0px になりました。

text
[パネル、numeric-only(既定)] 最初の高さ 0px
  開く: 0ms 71px / 50ms 71px / 100ms 71px / 150ms 71px / 200ms 71px / 250ms 71px / 300ms 71px / 350ms 71px
  閉じる: 0ms 0px / 50ms 0px / 100ms 0px / 150ms 0px / 200ms 0px / 250ms 0px / 300ms 0px / 350ms 0px

visibility も一緒に切り替えているのは、閉じたパネルの中のリンクに Tab キーでフォーカスが移らないようにするためです。MDN の visibility のページにあるとおり、hidden の要素はフォーカスを受け取れません。閉じた状態でボタンから Tab を押すと、visibility: hidden を外した版では見えていないリンクに、付けた版では後ろのボタンにフォーカスが移りました。

interpolate-size は、動かす要素か、その祖先に書く

interpolate-size の値は子孫に受け継がれます(上の MDN のページ)。:root に書けばページ全体に効きますが、範囲を絞りたいときは、高さを動かす要素そのものか、その祖先に書きます。:root を numeric-only に戻したうえで、書く場所を変えて測りました。

text
[パネル、.panel 自身に allow-keywords] 最初の高さ 0px
  開く: 0ms 0px / 50ms 25px / 100ms 47px / 150ms 61px / 200ms 68px / 250ms 71px / 300ms 71px / 350ms 71px
[パネル、子の .inner にだけ allow-keywords] 最初の高さ 0px
  開く: 0ms 71px / 50ms 71px / 100ms 71px / 150ms 71px / 200ms 71px / 250ms 71px / 300ms 71px / 350ms 71px

.panel 自身に書くと動きましたが、中の .inner にだけ書くと、一度に開いてしまいました。高さが変わるのは .panel なので、子に書いても効きません。

「続きを読む」のように、決まった高さから auto へ動かす

閉じた高さは 0 でなくてもかまいません。最初は3行分だけ見せて、ボタンで全文を出す「続きを読む」なら、閉じたときの高さを 4.8em にします。

css
/* 「続きを読む」:決まった高さから auto へ動かす */
.more {
  interpolate-size: allow-keywords;
}
.more .text {
  height: 4.8em;
  overflow: hidden;
  transition: height 300ms ease;
}
.more.is-open .text {
  height: auto;
}
.more .text p {
  margin: 0;
}
text
[続きを読む] 最初の高さ 77px
  開く: 0ms 77px / 50ms 103px / 100ms 128px / 150ms 142px / 200ms 150px / 250ms 153px / 300ms 154px / 350ms 154px
  閉じる: 0ms 154px / 50ms 127px / 100ms 102px / 150ms 88px / 200ms 81px / 250ms 77px / 300ms 77px / 350ms 77px

3行分の 77px から全文の 154px まで、300ms ほどかけて広がりました。

auto と min-content のような、キーワードどうしは動かない

allow-keywords にしても、両端がどちらも auto や min-content のようなキーワードだと補間しません。MDN のページにも「アニメーションの一方の端は <length-percentage> でなければなりません」とあります。interpolate-size: allow-keywords を書いたページで、箱の幅を切り替えて測りました。

text
[幅 0 から auto] 最初の幅 0px
  開く: 0ms 0px / 50ms 124px / 100ms 240px / 150ms 307px / 200ms 342px / 250ms 357px / 300ms 360px / 350ms 360px
[幅 min-content から auto] 最初の幅 144px
  開く: 0ms 360px / 50ms 360px / 100ms 360px / 150ms 360px / 200ms 360px / 250ms 360px / 300ms 360px / 350ms 360px
[幅 fit-content から auto] 最初の幅 144px
  開く: 0ms 360px / 50ms 360px / 100ms 360px / 150ms 360px / 200ms 360px / 250ms 360px / 300ms 360px / 350ms 360px

0 から auto へは動きましたが、min-content や fit-content から auto へは、押した直後に 360px になりました。閉じた側には 0 や 4.8em のような長さを書きます。

正式版で対応しているブラウザは Chromium 系だけ

@mdn/browser-compat-data 8.1.9 で、最初に使えるようになったバージョンを調べました。

機能 Chrome Edge Chrome(Android) Firefox Safari Safari(iOS)
interpolate-size 129 129 129 なし Technology Preview のみ なし
calc-size() 129 129 129 なし なし なし
@supports 28 12 28 22 9 9

interpolate-size と calc-size() に正式版で対応しているのは、表のうち Chrome、Edge、Android 版 Chrome だけです。Safari は、開発中の機能を試す Technology Preview 版だけに入っています。互換性データでも、どちらも実験的な機能とされています。

MDN の CSS のエラー処理のページによると、ブラウザは認識できないプロパティ名や不正な値の宣言を無視します。そのため、書き方によって、対応していないブラウザでの結果が変わります。

interpolate-size で書いた場合に、この1行だけが無視された状態を Chromium で作りました。panel.css の :root の行を既定値で打ち消し、代わりに存在しないプロパティ名 interpolate-sizex: allow-keywords を書いて測ると、height: auto は効いて、アニメーションなしで一度に開閉しました。

text
[panel.css の :root の行を既定値で打ち消し、存在しないプロパティ名 interpolate-sizex を書く] 最初の高さ 0px
  開く: 0ms 71px / 50ms 71px / 100ms 71px / 150ms 71px / 200ms 71px / 250ms 71px / 300ms 71px / 350ms 71px
  閉じる: 0ms 0px / 50ms 0px / 100ms 0px / 150ms 0px / 200ms 0px / 250ms 0px / 300ms 0px / 350ms 0px

calc-size() で書いた場合は、結果が違います。

注意

開いたときの高さを height: calc-size(auto, size) だけで書くと、calc-size() を知らないブラウザではパネルが開かなくなります。

calc-size() の行が無視された状態を作るため、開いたときの指定を、存在しない関数名 calc-sizex() だけにして測りました。ボタンを押しても、閉じたときの height: 0 が残り、高さは 0px のままでした。手前に height: auto を書いておくと、存在しない関数の行だけが無視され、一度に開くようになりました。

text
[開いたときを calc-sizex だけにする] 最初の高さ 0px
  開く: 0ms 0px / 50ms 0px / 100ms 0px / 150ms 0px / 200ms 0px / 250ms 0px / 300ms 0px / 350ms 0px
[開いたときを height: auto と calc-sizex にする] 最初の高さ 0px
  開く: 0ms 71px / 50ms 71px / 100ms 71px / 150ms 71px / 200ms 71px / 250ms 71px / 300ms 71px / 350ms 71px

calc-size() を使うなら、次のように height: auto を先に書きます。本物の calc-size() で書いたこの CSS は、Chromium では interpolate-size の場合とほぼ同じ動きで開閉しました(違いは 1px 以内)。

css
.panel.is-open {
  height: auto; /* calc-size() に対応していないブラウザ向け */
  height: calc-size(auto, size);
  visibility: visible;
}

なお、MDN の calc-size() のページは、ふだんは継承される interpolate-size で済ませ、calc-size() は auto の大きさに計算を加えたい場面に取っておくよう勧めています。

Firefox と Safari でも動かしたいなら、@supports で max-height と分ける

対応していないブラウザでも開閉を動かしたい場合は、昔からある max-height を動かす書き方を残し、対応しているブラウザでだけ height を auto へ動かします。ここでは、同じ .panel の指定を差し替えるだけで書き分けられる max-height を使いました。@supports は、ブラウザがその宣言に対応しているときだけ、中の CSS を使います。

css
/* 対応していないブラウザ向け:max-height を動かす */
.panel {
  max-height: 0;
  overflow: hidden;
  visibility: hidden;
  transition:
    max-height 300ms ease,
    visibility 300ms;
}
.panel.is-open {
  max-height: 20em;
  visibility: visible;
}
/* interpolate-size に対応していれば、height を auto へ動かす */
@supports (interpolate-size: allow-keywords) {
  :root {
    interpolate-size: allow-keywords;
  }
  .panel {
    max-height: none;
    height: 0;
    transition:
      height 300ms ease,
      visibility 300ms;
  }
  .panel.is-open {
    max-height: none;
    height: auto;
  }
}
.panel p {
  margin: 0;
  padding: 8px 0 12px;
}
@media (prefers-reduced-motion: reduce) {
  .panel {
    transition: none;
  }
}

@supports の中の .panel.is-open にも max-height: none を書いているのは、外側の .panel.is-open { max-height: 20em; } のほうが詳細度が高く、.panel に書いただけでは開いたときに上限が残るからです。

Chromium でこの CSS を開くと、CSS.supports('interpolate-size: allow-keywords') は true で、.panel には height の transition が使われていました。高さの動きも、最初の CSS とほぼ同じでした(違いは 2px 以内)。中身を 600px にして開くと、max-height は none のままで、20em(320px)で切れずに全部見えました。

text
[fallback.html] {"supports":true,"supportsCalcSize":true,"interpolateSize":"allow-keywords","transitionProperty":"height, visibility","maxHeight":"none"}
[fallback.html、中身 600px で開いた状態] max-height none / 高さ 620px
[maxheight.html、中身 600px で開いた状態] max-height 320px / 高さ 320px

最後の行のとおり、max-height だけの書き方では、上限の 20em を超える中身は切れます。また、中身の高さが分からないまま大きめの上限を決めるので、見た目の動きが、transition に書いた 300ms と合わなくなります。@supports の部分を外した CSS で測ると(次の出力は、コマごとの値の途中を「…」で省いています)、開くときは 50ms ほどで開ききり、閉じるときは押してから 126ms のコマまで高さが変わりませんでした。max-height は高さの上限なので、上限が 20em から中身の 71px まで下がるあいだは、高さが変わりません。

text
[max-height 0 から 20em] 最初の高さ 0px
  開く(コマごと): 1ms 0px / 17ms 12px / 34ms 36px / 51ms 71px / 67ms 71px / 84ms 71px / …
  閉じる(コマごと): 0ms 71px / 9ms 71px / 26ms 71px / 43ms 71px / 59ms 71px / 76ms 71px / 93ms 71px / 109ms 71px / 126ms 71px / 142ms 63px / 159ms 47px / 176ms 34px
ボタンを押してからのパネルの高さを、開くときと閉じるときに分けて並べた2つのグラフ。height を auto へ動かす水色の線は、開くときも閉じるときも 0px と 71px のあいだを 250ms ほどかけてなめらかに変わる。max-height を 20em へ動かす灰色の破線は、開くときは 50ms ほどで 71px に達し、閉じるときは 126ms まで 71px のまま横ばいで、そのあと急に下がる
パネルの高さの変わり方。水色が interpolate-size で height を auto へ動かした場合、灰色の破線が max-height を 20em へ動かした場合
ポイント

対応していないブラウザではアニメーションなしの開閉で構わないなら、@supports で分けずに :root へ1行足すだけで済みます。

OS の設定で画面の動きを減らしている人には、どちらの書き方でも prefers-reduced-motion で transition を外しています。Playwright で reducedMotion: 'reduce' を指定して最初の CSS を測ると、押した直後のコマで開ききりました。

​Posted by 86