【JavaScript】要素の高さを揃える方法|最大高さ取得・行ごとのグループ揃え・ResizeObserver・レスポンシブ対応まで解説

カード型レイアウトやナビゲーションメニューで「テキスト量が異なる要素の高さを揃えたい」場面はよくあります。CSSの Flexbox や Grid で解決できるケースも多いですが、「行ごとにグループ化して揃えたい」「タブ切替後に再計算したい」「サードパーティコンテンツが動的に高さを変える」などはJavaScriptでの実装が必要です。

この記事ではJavaScriptで高さを揃える基本実装から、行グループ対応・ResizeObserverによる自動追従・タブ・アコーディオン内の再計算まで、実践パターンを体系的に解説します。

スポンサーリンク

まず CSS で解決できるか確認する

同じコンテナ内の要素を揃えるだけなら、JavaScriptは不要です。

CSS Flexbox で高さを揃える(最も簡単)
.card-container {
  display: flex;
  align-items: stretch; /* デフォルト値。子要素の高さが自動で揃う */
  gap: 16px;
}

.card {
  flex: 1; /* 幅も均等に */
}
CSS Grid で揃える
.card-container {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
  /* align-items のデフォルトが stretch のため高さは自動で揃う */
}
CSSだけで解決できるケース:同じ親コンテナ内の直接の子要素を揃えるなら Flexbox / Grid が最善です。詳しくはFlexboxでCSS要素の高さを揃える方法も参照してください。
JavaScriptが必要なケース:①行ごとにカードをグループ化して揃えたい、②異なるコンテナにまたがる要素を揃えたい、③タブ・アコーディオンの切替後に再計算が必要、④動的に追加された要素への対応。

基本実装:最大高さを取得して設定する

複数の要素から最大の高さを求め、すべての要素に適用する基本パターンです。

setEqualHeight 関数
/**
 * 指定したセレクターの要素群の高さを最大値に揃える
 * @param {string|NodeList|Element[]} selector
 */
function setEqualHeight(selector) {
  const elements = typeof selector === 'string'
    ? Array.from(document.querySelectorAll(selector))
    : Array.from(selector);

  if (elements.length === 0) return;

  // 一度 height をリセットしてから計測(前回の値が残っていると正確に計れない)
  elements.forEach(el => { el.style.height = ''; });

  // offsetHeight ではなく getBoundingClientRect を使うと小数点精度で取れる
  const maxHeight = Math.max(...elements.map(el => el.getBoundingClientRect().height));

  elements.forEach(el => { el.style.height = `${maxHeight}px`; });
}

// 使用例
document.addEventListener('DOMContentLoaded', () => {
  setEqualHeight('.card');
});
必ず height をリセットしてから計測することが重要です。前回 style.height に値が入ったまま計測すると、その値が基準になって本来の高さが計れません。el.style.height = '' で一旦クリアしてから計測します。

行グループ揃え(レスポンシブ対応)

3列 → 2列 → 1列と変化するレスポンシブレイアウトでは、同じ行にある要素同士だけを揃える必要があります。全体で最大高さを揃えてしまうと、1列になったときに余白が大きくなりすぎます。

行ごとにグループ化して揃える
/**
 * 折り返しのあるグリッドで、同じ行の要素のみ高さを揃える
 * @param {string} selector
 */
function setEqualHeightByRow(selector) {
  const elements = Array.from(document.querySelectorAll(selector));
  if (elements.length === 0) return;

  // height リセット
  elements.forEach(el => { el.style.height = ''; });

  // top 座標が同じ要素を「同じ行」としてグループ化
  const rows = new Map();
  elements.forEach(el => {
    const top = Math.round(el.getBoundingClientRect().top);
    if (!rows.has(top)) rows.set(top, []);
    rows.get(top).push(el);
  });

  // 行ごとに最大高さを設定
  rows.forEach(rowElements => {
    const maxH = Math.max(...rowElements.map(el => el.getBoundingClientRect().height));
    rowElements.forEach(el => { el.style.height = `${maxH}px`; });
  });
}

document.addEventListener('DOMContentLoaded', () => {
  setEqualHeightByRow('.card');
});
なぜ getBoundingClientRect().top で行を判定するか:Flexbox や Grid でラップされた要素は、同じ行に並ぶ要素が同一の top 座標を持ちます。これを利用して「同じ行のグループ」を自動検出できます。小数誤差を防ぐために Math.round() で四捨五入しています。

ResizeObserver で自動再計算

ウィンドウのリサイズや動的コンテンツの変化に対応するには、ResizeObserver を使って要素サイズが変わったときに自動で再計算します。

ResizeObserver による自動対応
/**
 * 高さ揃えを ResizeObserver で自動再計算するクラス
 */
class EqualHeightWatcher {
  #selector;
  #observer;
  #resizeTimer = null;

  constructor(selector) {
    this.#selector = selector;

    // コンテナ要素のサイズ変化を監視
    this.#observer = new ResizeObserver(() => {
      // 連続して発火するのでデバウンス
      clearTimeout(this.#resizeTimer);
      this.#resizeTimer = setTimeout(() => this.#update(), 100);
    });
  }

  /** 監視を開始する */
  observe(container = document.body) {
    this.#update(); // 初回実行
    this.#observer.observe(container);
    return this;
  }

  /** 監視を停止する */
  disconnect() {
    this.#observer.disconnect();
    clearTimeout(this.#resizeTimer);
    return this;
  }

  /** 高さをリセットして再計算 */
  refresh() {
    this.#update();
    return this;
  }

  #update() {
    setEqualHeightByRow(this.#selector);
  }
}

// 使用例
const watcher = new EqualHeightWatcher('.card').observe(document.querySelector('.card-container'));

// 不要になったら監視解除
// watcher.disconnect();
window.resize イベントより ResizeObserver が優れている理由:window.resize はウィンドウ全体のリサイズにしか反応しませんが、ResizeObserver は特定要素のサイズ変化(サイドバーの折りたたみ・フォントサイズ変更など)にも対応できます。また要素単位で監視できるためパフォーマンスも優れています。

タブ・アコーディオン内の再計算

display: none の要素は getBoundingClientRect()0 を返します。タブやアコーディオン内の要素の高さを揃えるには、表示してから計測するか、一時的に可視にして計測する必要があります。

タブ切替後に再計算
// タブ切替時に高さを再計算する例
const tabs = document.querySelectorAll('[data-tab]');
const panels = document.querySelectorAll('[data-panel]');

tabs.forEach(tab => {
  tab.addEventListener('click', () => {
    const target = tab.dataset.tab;

    // パネルの切替
    panels.forEach(p => p.hidden = true);
    const activePanel = document.querySelector(`[data-panel="${target}"]`);
    activePanel.hidden = false;

    // パネルが表示されてから高さを揃える
    // hidden → visible の反映には1フレーム待つのが確実
    requestAnimationFrame(() => {
      setEqualHeightByRow(`[data-panel="${target}"] .card`);
    });
  });
});
display:none の要素を一時的に可視化して計測
/**
 * display:none の要素を一時的に可視にして高さを取得するユーティリティ
 */
function getHeightWhileHidden(element) {
  // 視覚的に隠しつつレイアウトを有効にする
  const prev = {
    visibility: element.style.visibility,
    display:    element.style.display,
    position:   element.style.position,
  };

  element.style.visibility = 'hidden';
  element.style.display    = 'block';
  element.style.position   = 'absolute';

  const height = element.getBoundingClientRect().height;

  // 元に戻す
  element.style.visibility = prev.visibility;
  element.style.display    = prev.display;
  element.style.position   = prev.position;

  return height;
}
アコーディオンの場合:アコーディオンを開くアニメーションに max-height トランジションを使っている場合、アニメーション完了後に transitionend イベントで高さを再計算するとより正確です。

高さ以外のプロパティも揃える(汎用版)

高さだけでなく、最小高さ(minHeight)や幅(width)を揃えたい場合にも使える汎用関数です。

汎用:指定プロパティを最大値に揃える
/**
 * 指定したプロパティを最大値に揃える汎用関数
 * @param {string} selector
 * @param {'height'|'minHeight'|'width'} property
 */
function setEqualProperty(selector, property = 'height') {
  const elements = Array.from(document.querySelectorAll(selector));
  if (elements.length === 0) return;

  // リセット
  elements.forEach(el => { el.style[property] = ''; });

  // 寸法プロパティのマッピング
  const dimensionMap = {
    height:    'height',
    minHeight: 'height',
    width:     'width',
    minWidth:  'width',
  };
  const dimension = dimensionMap[property] ?? 'height';

  const maxVal = Math.max(
    ...elements.map(el => el.getBoundingClientRect()[dimension])
  );

  elements.forEach(el => { el.style[property] = `${maxVal}px`; });
}

// 高さを揃える
setEqualProperty('.card', 'height');

// 幅を揃える
setEqualProperty('.nav-item', 'width');

// minHeight で揃える(コンテンツが多い場合は自然に伸びる)
setEqualProperty('.card', 'minHeight');
height vs minHeightheight を固定すると、フォントサイズ変更時や多言語対応時にコンテンツがはみ出す場合があります。minHeight で揃えると、最低限の高さを確保しながらコンテンツが多い要素は自然に伸びるため、より堅牢です。

実用例:カードコンポーネントへの適用

HTML(カードレイアウトの例)
<div class="card-container">
  <div class="card">
    <div class="card-image"><img src="image1.jpg" alt="商品1"></div>
    <div class="card-body">
      <h3 class="card-title">商品名A</h3>
      <p class="card-desc">短い説明</p>
    </div>
    <div class="card-footer"><button>詳細を見る</button></div>
  </div>
  <div class="card">
    <div class="card-image"><img src="image2.jpg" alt="商品2"></div>
    <div class="card-body">
      <h3 class="card-title">商品名B(長いタイトル)</h3>
      <p class="card-desc">こちらは説明文が長くなっています。複数行にまたがる場合でも高さを揃えたいケースです。</p>
    </div>
    <div class="card-footer"><button>詳細を見る</button></div>
  </div>
</div>
カード内のパーツ別に揃える
document.addEventListener('DOMContentLoaded', () => {
  // カード全体を揃えるのではなく、パーツごとに揃える(より正確なレイアウト)
  setEqualHeightByRow('.card-title');   // タイトルの高さを揃える
  setEqualHeightByRow('.card-desc');    // 説明文の高さを揃える
  setEqualHeightByRow('.card-body');    // ボディ全体を揃える

  // ウィンドウリサイズ時に再計算
  const watcher = new EqualHeightWatcher('.card-title, .card-desc, .card-body')
    .observe(document.querySelector('.card-container'));
});
なぜカード全体でなくパーツごとに揃えるか:カード全体の高さを揃えると「ボタンが下に固定されない」問題が起きます。タイトル・説明文・ボディをそれぞれ揃えることで、フッターのボタンが常に同じ位置に並びます。CSSの margin-top: auto(Flexbox)と組み合わせるとさらに綺麗に揃います。

よくある質問(FAQ)

QCSS の Flexbox で解決できるのに JavaScript を使う必要がありますか?
A同じコンテナ内の直接の子要素を揃えるだけなら CSS の Flexbox / Grid で十分です。JavaScriptが必要なのは「行グループごとに揃える」「異なるコンテナをまたぐ」「タブ切替後に再計算」「動的コンテンツへの自動追従」などのケースです。
Q高さを揃えた後にフォントサイズが変わると崩れます。対策は?
Aheight の代わりに minHeight を使うと、コンテンツが増えても自然に伸びます。また ResizeObserver で要素を監視しておけば、フォントサイズ変更によるリフロー後に自動で再計算されます。
Qdisplay:none の要素の高さが 0 になってしまいます。
Adisplay: none の要素はレイアウトから除外されるため、getBoundingClientRect()0 を返します。visibility: hidden + position: absolute に切り替えて計測する「一時可視化」テクニックを使うか、要素が表示されてから(requestAnimationFrametransitionend で)計測するようにします。
Qwindow.resize で対応するのと ResizeObserver の違いは何ですか?
Awindow.resize はウィンドウ全体のリサイズにしか反応しません。ResizeObserver は特定の要素のサイズ変化(サイドバーの折りたたみ・動的コンテンツの変化など)を直接監視できます。またパフォーマンス上も ResizeObserver の方が効率的です。
Q画像が読み込まれる前に高さ揃えを実行すると正しくないサイズになります。
A画像の読み込み完了後に実行するには window.addEventListener("load", ...) を使います(DOMContentLoaded では画像は読み込まれていません)。または各 img 要素の load イベント、あるいは Promise.all で全画像の読み込みを待ってから実行する方法もあります。

まとめ

要素の高さを揃える方法を用途別に整理します。

状況 推奨手法
同コンテナ内の子要素を揃える CSS Flexbox(align-items: stretch
行グループごとに揃える(レスポンシブ) getBoundingClientRect().top でグループ化 + setEqualHeightByRow
ウィンドウリサイズ・動的変化に追従 ResizeObserver + デバウンス
タブ・アコーディオン切替後 requestAnimationFrame または transitionend 後に再計算
コンテンツ増加に柔軟対応 height の代わりに minHeight を使う
display:none 要素の計測 一時的に visibility: hidden + display: block に切り替えて計測

要素の高さ・幅の取得方法(offsetHeight・clientHeight・getBoundingClientRect の違い)については要素の幅・高さを取得する方法を、CSSだけで揃えるアプローチはFlexboxでCSS要素の高さを揃える方法もあわせて参照してください。