height:autoがアニメーションしない理由と実務的な直し方|max-heightの限界・grid 0fr→1fr・JSで実測値を渡す3手順

height:autoがアニメーションしない理由と実務的な直し方|max-heightの限界・grid 0fr→1fr・JSで実測値を渡す3手順

結論:height:auto はそもそも補間できない。ごまかすか、別のプロパティに置き換えるかの二択で、実務の既定解は grid-template-rows の 0fr→1fr

  • 原因=transitionは数値同士でしか補間できず、autoは計算前の「後で決まる」という指示なので始点も終点も存在しない
  • max-heightで大きめの値を入れる方法は動くが、余った分だけ開くのが遅れ閉じるのが速い不自然な動きになる。項目数が可変のFAQでは破綻する
  • CSSだけで完結させるなら grid-template-rows: 0fr → 1fr。子要素を1枚ラップして overflow:hidden、paddingは子ではなくラッパーの内側に置く
  • 閉じきらない事故の9割は、子のpadding・min-height・borderが 0fr でも残っているだけ。ラッパー側に寄せれば直る
  • 高さを厳密に制御したい/中身が動的に変わるならJSでscrollHeightを渡す。ただし開いた後は height:auto に戻さないと、中身が増えたとき見切れる

アコーディオン、FAQ、ドロワーの中のサブメニュー。開閉のたびにふわっと開いてほしいのに、パッと出てパッと消えるだけで、transition: height .3s がまったく効いていない。この現象は、書き方を間違えているのではなく、CSSの仕様上そう動くのが正しいために起きます。

この記事では、なぜ効かないのかという仕組みから、実務で採れる3つの手当て、そして実装後に必ず一度は踏む落とし穴までを、切り分けの順番どおりに書きます。

なぜ height:auto はアニメーションしないのか

transition は、始点と終点の数値の間を埋める仕組みです。0px から 240px なら、その間の 120px という値が計算できます。

ところが auto は数値ではありません。「中身とレイアウトの結果に応じて、後でブラウザが決める」という指示です。レイアウトが終わるまで具体的な値が存在しないので、0px と auto の中間という値をブラウザは作れません。作れない以上、補間はあきらめて始点から終点へ一瞬で飛ぶ——これが「効かない」の正体です。

同じ理由で、次のような組み合わせもすべてアニメーションしません。原因は共通なので、どれか1つで悩んでいるなら他も同じ手当てになります。

  • height: 0 ↔ height: auto
  • width: 0 ↔ width: auto(横に開くメニュー)
  • height: auto ↔ height: fit-content / max-content
  • display: none ↔ display: block(そもそも補間対象外。要素が消えるので transition の出番がない)

なお display については、近年 transition-behavior: allow-discrete と @starting-style という受け皿が入りつつあります。ただし実案件では対応していない環境で「開かない」ではなく「アニメーションなしで開く」に落ちる書き方にしておく必要があるので、後述の方法と組み合わせる前提で考えたほうが安全です。

max-height でごまかす方法と、その限界

最も広く出回っている回避策が、height の代わりに max-height を動かす方法です。

.accordion__body {
  max-height: 0;
  overflow: hidden;
  transition: max-height .3s ease;
}
.accordion__body.is-open {
  max-height: 800px; /* 中身より十分大きい値 */
}

これは確かに動きます。800px は具体的な数値なので補間できるからです。ただし、この方法には構造的に避けられない副作用があります。

中身の実高さが 180px しかないのに終点を 800px にした場合、ブラウザは 0 → 800px を0.3秒かけて進みます。つまり実際に見えている部分は最初の約0.07秒で開ききり、残りの0.23秒は何も起きていない時間になります。逆に閉じるときは、最初の0.23秒で見えない領域が縮み、最後の一瞬で中身が畳まれる。結果として開くのは妙に速く、閉じるのはワンテンポ遅れてから急に消えるという、原因の説明しづらい不自然さが残ります。

そして致命的なのは、終点の値を決め打ちしなければならないという点です。FAQの回答文をCMSで編集できるようにしている、開閉の中に画像が入る、スマホで折り返し行数が増える——こうした「高さが後から変わる」案件では、800px を超えた瞬間にアニメーションが効かなくなるのではなく、中身が途中で切れるという、より発見の遅いバグに化けます。

結論として、max-height方式が許されるのは中身が固定で、高さが絶対に増えないと言い切れる箇所だけです。それ以外では次の方法を使います。

現実解①:grid-template-rows を 0fr → 1fr にする

CSSだけで完結させるなら、これが現在の既定解です。grid の行サイズは fr という数値単位で指定でき、0fr と 1fr の間は補間できるという性質を使います。1fr はコンテンツの実高さに落ち着くので、高さを決め打ちせずに「中身ぴったり」までアニメーションできるのが最大の利点です。

.accordion__body {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows .3s ease;
}
.accordion__body.is-open {
  grid-template-rows: 1fr;
}
/* 中身は必ず1枚ラップして、はみ出しを隠す */
.accordion__inner {
  overflow: hidden;
  min-height: 0;
}
<div class="accordion__body">
  <div class="accordion__inner">
    <p>回答本文がここに入ります。</p>
  </div>
</div>

ポイントは3つあります。

  • ラッパーを1枚必ず入れる。overflow: hidden を掛ける相手は grid コンテナ側ではなく、行の中身側です
  • ラッパーに min-height: 0 を付ける。grid アイテムの既定の最小サイズはコンテンツ由来で、これを打ち消さないと 0fr にしても縮みません(flexアイテムが縮まない原因と同じ min-width: auto / min-height: auto の話です)
  • padding は子ではなくラッパーの内側に置く。理由は次の落とし穴で説明します

閉じきらないときに確認する3点

実装してまず出る不具合が「閉じたのに数十pxだけ残る」です。原因はほぼこの3つに限られます。

  • ラッパー自身に padding が付いている:行の高さが 0fr になっても、overflow: hidden の内側にある padding は高さとして残ります。padding はさらに内側の要素に移すか、開閉に合わせて padding も transition させます
  • ラッパーや子に border がある:padding と同じ理由です。境界線は閉じた側の要素(見出しボタン側)に持たせるのが定石です
  • 子要素の margin がはみ出している:overflow: hidden があるとマージンの相殺は起きませんが、子の margin-top がそのまま高さに乗ります。ラッパー内は > * + * { margin-block-start: … } のように、先頭要素に上マージンが付かない書き方にしておくと安定します

現実解②:JSで実測値を渡し、開ききったら auto に戻す

高さを厳密に制御したい、開閉と同時に別の処理を走らせたい、あるいは開いている最中に中身が変わる(タブ切り替え、遅延読み込み画像)——こうしたケースでは、JSで実高さを測って渡します。

function toggle(panel, open) {
  const h = panel.scrollHeight;          // 中身の実高さ
  if (open) {
    panel.style.height = h + 'px';
    panel.addEventListener('transitionend', function onEnd(e) {
      if (e.propertyName !== 'height') return;
      panel.style.height = 'auto';       // 開ききったら auto に戻す
      panel.removeEventListener('transitionend', onEnd);
    });
  } else {
    panel.style.height = h + 'px';       // auto から実数値に固定し直す
    panel.getBoundingClientRect();       // 強制的にレイアウトを確定させる
    panel.style.height = '0px';
  }
}

この書き方には、見落とすと確実にハマる要点が2つあります。

1つめは、開ききったら height: auto に戻すこと。240px のまま固定しておくと、後から中身が増えたとき(画像が読み込まれた、文字サイズを変えた、スマホで折り返しが増えた)に下が見切れます。開くアニメーションが終わった瞬間に auto へ返すのが正解です。

2つめは、閉じるときは「auto → 0」ではなく「実数値 → 0」の二段にすること。auto のまま 0px を代入しても、①で説明したとおり補間できず一瞬で閉じます。いったん scrollHeight を代入し、そのレイアウトをブラウザに確定させてから 0 にする必要があります。上のコードの getBoundingClientRect() は、値を使うためではなくこの確定を強制するためだけに呼んでいます。この1行を消すと、ブラウザが2つの代入をまとめて処理してしまい、閉じるときだけアニメーションが消えます。

<details> を使っている場合

<details> / <summary> はアクセシビリティとキーボード操作を無料で手に入れられる反面、閉じる瞬間に中身が即座に消えるため、そのままでは閉じるアニメーションが成立しません。open 属性が外れた時点で中身が描画対象から外れるからです。

実務では、クリックを一度横取りして、閉じるアニメーションが終わってから open を外すという形にします。

summary.addEventListener('click', (e) => {
  if (!details.open) return;     // 開くときは既定動作に任せる
  e.preventDefault();            // 閉じるときだけ横取り
  closeWithAnimation(details);   // アニメーション後に details.open = false
});

ここまでやる必要があるかは案件次第です。閉じるアニメーションを諦めて開くときだけ動かすという割り切りも、実装コストと不具合リスクを考えれば十分に妥当な判断です。

どれを選ぶかの判断基準

3つの方法は優劣ではなく適材適所です。迷ったら次の順で当てはめます。

  • 中身が固定・高さが絶対に増えない小さなパネル:max-height でよい。ただし終点は実高さの1.5倍程度に抑え、2000px のような雑な値は入れない(待ち時間がそのまま不自然さになります)
  • FAQ・サブメニューなど、CSSだけで完結させたい大多数のケース:grid-template-rows の 0fr→1fr。ラッパー1枚と min-height: 0 を忘れない
  • 開いている最中に中身が変わる/開閉と連動した処理がある:JSで scrollHeight を渡す。開ききったら auto に戻す

また、どの方法を採る場合でもアニメーションを切る受け皿は用意しておきます。動きに酔いやすい利用者への配慮であると同時に、不具合の切り分けが劇的に楽になるためです。

@media (prefers-reduced-motion: reduce) {
  .accordion__body { transition: none; }
}

実装後に一度は疑うこと

「書いたとおりなのに動かない」ときは、開閉のCSSより先に外側の条件を確認したほうが早いことが多いです。

  • 親に overflow: hidden や content-visibility: auto が掛かっていないか:高さの計算が変わり、scrollHeight が意図しない値を返すことがあります
  • 同じ要素に別の transition が二重に効いていないか:transition: all を使っていると、他の変化に巻き込まれて挙動が読めなくなります。all は使わず、プロパティを明示します
  • 開閉のトリガーがボタンになっているか:<div> にクリックを付けているだけだと、キーボードで開けません。<button type="button"> と aria-expanded を付けるだけで、実装量を増やさずに操作性が揃います

アコーディオンは「よくある部品」に見えて、CSSがサイズを決める仕組みそのものに一度触らないと解けないという、実務でありがちな構造をしています。逆に言えば、ここで auto の意味と min-height: 0 の役割を押さえておくと、レイアウトが縮まない・はみ出すといった別の不具合も同じ物差しで見られるようになります。

関連記事(コーディング実務Tips)

多言語・ベトナム語サイトの制作でお困りではありませんか?

フォント・文字化け・WordPress多言語化・サーバー選定まで、多言語サイト制作の実績を持つ制作会社が一括対応します。お見積り・ご相談は無料です。

多言語サイト制作の詳細を見る →

すぐに相談したい方は お問い合わせフォームへ

Web制作Tips(コーディング実務)カテゴリの最新記事