カード型レイアウトやナビゲーションメニューで「テキスト量が異なる要素の高さを揃えたい」場面はよくあります。CSSの Flexbox や Grid で解決できるケースも多いですが、「行ごとにグループ化して揃えたい」「タブ切替後に再計算したい」「サードパーティコンテンツが動的に高さを変える」などはJavaScriptでの実装が必要です。
この記事ではJavaScriptで高さを揃える基本実装から、行グループ対応・ResizeObserverによる自動追従・タブ・アコーディオン内の再計算まで、実践パターンを体系的に解説します。
まず CSS で解決できるか確認する
同じコンテナ内の要素を揃えるだけなら、JavaScriptは不要です。
.card-container {
display: flex;
align-items: stretch; /* デフォルト値。子要素の高さが自動で揃う */
gap: 16px;
}
.card {
flex: 1; /* 幅も均等に */
}
.card-container {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
/* align-items のデフォルトが stretch のため高さは自動で揃う */
}
JavaScriptが必要なケース:①行ごとにカードをグループ化して揃えたい、②異なるコンテナにまたがる要素を揃えたい、③タブ・アコーディオンの切替後に再計算が必要、④動的に追加された要素への対応。
基本実装:最大高さを取得して設定する
複数の要素から最大の高さを求め、すべての要素に適用する基本パターンです。
/**
* 指定したセレクターの要素群の高さを最大値に揃える
* @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');
});
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 で自動再計算するクラス
*/
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 は特定要素のサイズ変化(サイドバーの折りたたみ・フォントサイズ変更など)にも対応できます。また要素単位で監視できるためパフォーマンスも優れています。タブ・アコーディオン内の再計算
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 の要素を一時的に可視にして高さを取得するユーティリティ
*/
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 minHeight:height を固定すると、フォントサイズ変更時や多言語対応時にコンテンツがはみ出す場合があります。minHeight で揃えると、最低限の高さを確保しながらコンテンツが多い要素は自然に伸びるため、より堅牢です。実用例:カードコンポーネントへの適用
<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'));
});
margin-top: auto(Flexbox)と組み合わせるとさらに綺麗に揃います。よくある質問(FAQ)
height の代わりに minHeight を使うと、コンテンツが増えても自然に伸びます。また ResizeObserver で要素を監視しておけば、フォントサイズ変更によるリフロー後に自動で再計算されます。display: none の要素はレイアウトから除外されるため、getBoundingClientRect() は 0 を返します。visibility: hidden + position: absolute に切り替えて計測する「一時可視化」テクニックを使うか、要素が表示されてから(requestAnimationFrame や transitionend で)計測するようにします。window.resize はウィンドウ全体のリサイズにしか反応しません。ResizeObserver は特定の要素のサイズ変化(サイドバーの折りたたみ・動的コンテンツの変化など)を直接監視できます。またパフォーマンス上も ResizeObserver の方が効率的です。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要素の高さを揃える方法もあわせて参照してください。