object-fitとaspect-ratioが効かない・画像の縦横比が崩れる原因と直し方|高さが決まっていないボックスとflex/gridのstretchを疑う

object-fitとaspect-ratioが効かない・画像の縦横比が崩れる原因と直し方|高さが決まっていないボックスとflex/gridのstretchを疑う

結論:object-fitが効かないのは書き方の問題ではなく、画像のボックスに高さが無いから。まず「誰が高さを決めているか」を確定させる

  • object-fitが働く条件=画像のボックスの幅と高さが両方決まっていること。height:auto のままでは元の比率どおりに表示されるだけで、object-fitは仕事をしない
  • 正しい型=比率は親(またはimg自身)の aspect-ratio で決め、imgは width:100% / height:100% / object-fit:cover でその箱を満たす。この3点セットで崩れは止まる
  • 勝手に縦長になる犯人=flexコンテナの align-items:stretch。imgが直下の子だと親の高さまで引き伸ばされる。align-self:start か、img をdivで包んで解決する
  • aspect-ratioが無視される条件=widthとheightが両方確定しているとき。リセットCSSの height:auto は邪魔ではなく、aspect-ratioを効かせるために必要
  • HTMLの width / height 属性は装飾ではなくCLS予約。CSS側で height:auto を当てる前提で必ず両方書く
  • 切り分けの最短手順=コンソールで naturalWidth/naturalHeight と実表示サイズの比を突き合わせ、ズレている画像だけを機械的に洗い出す

カード一覧を組んだら、1枚だけ画像が縦に間延びしている。object-fit: cover と書いたのにトリミングされず全体が潰れて入っている。aspect-ratio: 16 / 9 を指定したのにまるで無視される。開発者ツールで見ても打ち消している宣言は見当たらない——。

この3つは別々の不具合に見えて、原因はほぼ同じ一点に集約されます。「その画像のボックスの高さを、最終的に誰が決めているのか」が曖昧になっているだけです。object-fit はボックスの中身をどう収めるかを決めるプロパティであって、ボックスの大きさを決めるプロパティではありません。箱が決まっていなければ、収め方を指定しても何も起きません。

この記事では、なぜ効かないのかという仕組みから、崩れている画像を機械的に特定する手順、そして実務でそのまま使える直し方の型までを、切り分けの順番どおりに書きます。

目次

まず「崩れている」を3類型に切り分ける

画像の崩れは、原因別に3つしかありません。最初にどれなのかを決めてしまうと、以降の判断がぶれません。

① 比率は正しいが大きさが揃っていない

カードごとに画像の高さがバラバラで、テキストの開始位置が揃わないタイプです。これは崩れではなくボックスの高さを誰も決めていない状態で、比率自体は元画像どおりです。aspect-ratio で箱の比率を決めるのが正解で、object-fit はその後に効いてきます。

② 比率が壊れて縦長・横長に潰れている

人物の顔が縦に伸びる、ロゴが横に潰れる、という見てすぐ分かるタイプです。ボックスの幅と高さが両方決まっているのに object-fit が指定されていないか、flex / grid が勝手に片方の軸を引き伸ばしているかのどちらかです。実務で最も多いのはこの2つ目です。

③ ボックスからはみ出している

親の幅を超えて横スクロールが出る、角丸の外に画像が出る、というタイプです。max-width: 100% の欠落か、grid / flex アイテムの min-width: auto / min-height: auto が縮小を止めています。横スクロールが出ている場合の犯人特定はスマホで横スクロールが出る原因の特定手順が早いので、そちらと併せて見てください。

以降は、実務で圧倒的に多い①と②を潰していきます。

object-fitが効かない最大の原因は「ボックスの高さが決まっていない」

object-fit は、置換要素(img / video など)の中身を、要素のボックスに対してどう収めるかを指定するプロパティです。ここで重要なのは、ボックスの大きさは別の誰かが決めているという点です。

height:auto のままでは object-fit は仕事をしない

ほとんどのリセットCSSには、次の1行が入っています。

img {
  max-width: 100%;
  height: auto;
}

この状態で object-fit: cover を足しても、見た目は1ピクセルも変わりません。height: auto は「元画像の比率で高さを決める」という意味なので、ボックスの比率と中身の比率が最初から一致しているからです。収め方を指定しても、収め直す必要がそもそも発生していません。

/* 効かない例:箱の高さが中身任せなので object-fit の出番が無い */
.card__img {
  width: 100%;
  height: auto;        /* ← 中身の比率がそのまま箱の比率になる */
  object-fit: cover;   /* ← 何も起きない */
}

つまり object-fit が効いていないのではなく、効かせる状況になっていないのが実態です。「cover を書いたのにトリミングされない」という相談の大半はこれで説明がつきます。

正しい型は「箱の比率を決める」「中身で満たす」の2段構え

直し方はいつも同じ形になります。箱の比率は aspect-ratio で決め、画像はその箱を 100% で満たして object-fit で収め方を指定する——この型を覚えておけば、カード・サムネイル・ヒーロー画像のすべてに使い回せます。

/* 推奨:imgに直接比率を持たせる最小構成 */
.card__img {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;   /* 箱の比率を確定させる */
  height: auto;           /* 高さは比率から算出(ここは auto のままでよい) */
  object-fit: cover;      /* はみ出す分をトリミング */
  object-position: 50% 40%; /* 切れる位置を調整(人物なら少し上寄せ) */
}

親要素でボックスを作ってから画像を敷く書き方も同じ考え方です。<picture> や遅延読み込みのプレースホルダを挟む構成では、こちらのほうが扱いやすくなります。

/* 親で箱を作る構成(picture・プレースホルダを挟むとき向き) */
.card__figure {
  aspect-ratio: 16 / 9;
  overflow: hidden;       /* はみ出しと角丸の処理はここで */
  border-radius: 8px;
}
.card__figure img {
  display: block;
  width: 100%;
  height: 100%;           /* ← 親の高さを100%受け取るのが要点 */
  object-fit: cover;
}

この構成で height: 100% を書き忘れると、リセットCSSの height: auto が残り、画像は箱の上辺に元比率のまま貼り付いて下に隙間ができます。「親で箱を作ったら、子は必ず height:100%」はセットで覚えてください。

display: block を忘れると下に4pxの隙間が出る

ついでに定番の事故を1つ。img は初期値がインライン要素なので、行のベースライン分の余白が画像の下に残ります。箱の比率をきっちり作ったのに数ピクセルだけ下に隙間ができるのはこれです。display: block(または親に line-height: 0)で消えます。角丸やオーバーレイを重ねたときに気づきやすい症状です。

勝手に縦長になる犯人は flex / grid の align-items: stretch

「CSSは何も変えていないのに、カードを横並びにした途端に画像だけ縦に伸びた」——これは object-fit 以前の問題で、レイアウト側が画像の高さを書き換えています。

再現:imgがflexアイテムになると親の高さまで引き伸ばされる

display: flex の初期値は align-items: stretch です。つまり行の中で一番高いアイテムに合わせて、他のアイテムが交差軸方向に引き伸ばされます。img がflexコンテナの直下の子になっていると、画像もこの引き伸ばしの対象になります。

<!-- imgがflexの直接の子:テキストが長いカードほど画像が縦に伸びる -->
<div class="media">
  <img src="thumb.jpg" alt="">
  <p>テキストが長くなるほど、この行の高さが伸びる</p>
</div>

<style>
.media { display: flex; gap: 16px; }
.media img { width: 160px; }  /* height指定なし=stretchで縦に伸びる */
</style>

厄介なのは、テキストの長さで崩れ方が変わることです。開発中の短いダミーテキストでは再現せず、実データを流し込んだ瞬間に崩れます。「ステージングでは平気だったのに本番で崩れた」はたいていこのパターンです。

直し方は3つ。原則は「imgをレイアウトの直接の子にしない」

/* 1. 交差軸の引き伸ばしを個別に止める(最短) */
.media img { align-self: flex-start; }

/* 2. 比率を確定させて引き伸ばしを無効化する(推奨) */
.media img {
  width: 160px;
  aspect-ratio: 4 / 3;
  height: auto;
  object-fit: cover;
}

/* 3. divで包み、レイアウトと画像の責務を分ける(大規模向け) */
.media__thumb { flex: 0 0 160px; aspect-ratio: 4 / 3; overflow: hidden; }
.media__thumb img { width: 100%; height: 100%; object-fit: cover; }

単発の修正なら1、新規実装なら2か3を選びます。3の「レイアウト用の箱と画像を分ける」形にしておくと、後からアスペクト比を変えたい・ホバーでズームさせたいといった要望が出ても、画像側のCSSを触らずに済みます。運用で効いてくるのはここです。

gridで画像がはみ出すのは min-height: auto

gridアイテムの min-width / min-height の初期値は auto で、中身の最小サイズより小さくならないという制約が入ります。画像を入れたgridセルが指定した 1fr を無視して膨らむのはこれが原因です。

.grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); }
/* または */
.grid > * { min-width: 0; min-height: 0; }

flexで文字が省略されない・縮まないのも同じ仕様が原因です。仕組みの詳細はflexアイテムが縮まない原因はmin-width:autoにまとめてあるので、レイアウトごと崩れている場合はそちらを先に読むと早く終わります。

aspect-ratioが効かないのは「幅と高さが両方確定しているとき」

aspect-ratio は片方の軸から、もう片方の軸を計算するプロパティです。したがって両方の軸がすでに確定していると、計算する余地が無いので無視されます。これが「aspect-ratioが効かない」の正体です。

/* 効かない:widthもheightも確定しているので比率の出番が無い */
.thumb {
  width: 320px;
  height: 320px;      /* ← これが勝つ */
  aspect-ratio: 16 / 9;  /* 無視される */
}

/* 効く:片方をautoにして、比率に計算させる */
.thumb {
  width: 320px;
  height: auto;       /* ← 比率から算出される */
  aspect-ratio: 16 / 9;
}

ここでリセットCSSの img { height: auto; } が効いてきます。これは aspect-ratio の邪魔をしているのではなく、むしろ効かせるために必要な指定です。消してしまうと、後述するHTML属性由来の比率まで一緒に効かなくなります。

flex / grid の中では aspect-ratio も上書きされる

もう一つの盲点が、レイアウトによって高さが確定させられているケースです。flexの stretch やgridの 1fr 行に置かれたアイテムは、幅も高さもレイアウト側で決まってしまうため、aspect-ratio は無視されます。「単体では効くのに、一覧に入れた途端に効かなくなる」場合はこれを疑ってください。align-self: start を当てて高さの確定を外すと復活します。

HTMLの width / height 属性は装飾ではなくCLS予約

モダンブラウザは <img> の width / height 属性から内部的に aspect-ratio を組み立て、画像のダウンロードが終わる前に場所を確保します。属性を省くと、画像が届いた瞬間に後続のコンテンツが押し下げられ、CLS(レイアウトシフト)が発生します。

<!-- 属性は実寸のピクセル値をそのまま書く。CSS側で height:auto を当てる前提 -->
<img src="hero.webp" width="1600" height="900" alt="商品一覧ページのカード表示">

ここで width / height 属性はCSSの width: 100% に上書きされるので、レイアウトは崩れません。「属性を書くと大きさが固定されてしまう」という誤解で外してしまうと、CLSだけが悪化します。片方だけ書くのも無効で、必ず両方です。画像とLCP・CLSの関係はloading=”lazy”を全画像に付けるとLCPが悪化するで別途整理しています。

崩れている画像だけを機械的に洗い出すスニペット

目視で探すと必ず取りこぼします。「元画像の比率」と「実際に表示されている比率」を突き合わせて、ズレている画像だけを列挙するのが確実です。コンソールにそのまま貼れる形にしてあります。

// 表示比率が元比率から2%以上ズレているimgを列挙する
(() => {
  const rows = [];
  document.querySelectorAll('img').forEach((img) => {
    if (!img.naturalWidth || !img.naturalHeight) return; // 未読込はスキップ
    const r = img.getBoundingClientRect();
    if (r.width < 8 || r.height < 8) return;             // 計測ピクセル等を除外
    const natural = img.naturalWidth / img.naturalHeight;
    const shown   = r.width / r.height;
    const diff    = Math.abs(shown - natural) / natural;
    const cs      = getComputedStyle(img);
    if (diff > 0.02 && cs.objectFit === 'fill') {
      rows.push({
        src: img.currentSrc.split('/').pop(),
        元比率: natural.toFixed(3),
        表示比率: shown.toFixed(3),
        ズレ: (diff * 100).toFixed(1) + '%',
        objectFit: cs.objectFit,
        親のdisplay: getComputedStyle(img.parentElement).display,
        属性width: img.getAttribute('width'),
        属性height: img.getAttribute('height'),
        el: img
      });
    }
  });
  console.table(rows);
  rows.forEach(r => r.el.style.outline = '3px solid magenta');
  console.log('崩れ候補:', rows.length, '件');
})();

ポイントは object-fit が初期値の fill のままかどうかを同時に見ているところです。cover や contain が指定されていれば、比率がズレていても意図的なトリミングなので除外できます。残った行が、そのまま修正対象のリストになります。

出力の 親のdisplay 列も重要です。ここが flex か grid になっていれば、前章の引き伸ばしが原因だと即断できます。属性width / 属性height が null の行は、崩れと同時にCLSの原因にもなっている画像です。1回の実行で「比率の崩れ」と「CLSの穴」の両方が拾えるので、納品前チェックの定番として置いておくと効きます。

実務で踏んだ落とし穴6件

① object-fit: cover で顔が切れる → object-position で逃がす

cover は中央基準でトリミングするため、人物写真では顎から上、商品写真では上部が切れます。object-position: 50% 30% のように縦方向だけ上に寄せるのが実用的な逃がし方です。CMSで画像を差し替える運用なら、記事カードは 50% 35% 前後を既定にしておくと事故が減ります。

② 比率を崩したくない画像に cover を当ててしまう

ロゴ・図解・スクリーンショットは切れると意味が壊れます。これらは object-fit: contain を使い、余白は親の background-color で埋めます。写真は cover、意味のある画像は contain、と用途で分けるのが安全です。

③ background-image に逃げるとLCPが遅くなる

トリミングだけが目的なら background-size: cover でもできますが、CSSの背景画像はHTMLのプリロードスキャナに発見されないため、CSSを読み終わるまでダウンロードが始まりません。ファーストビューの主役画像でこれをやると、LCPが目に見えて悪化します。表示要素としての画像は必ず <img>、装飾だけ背景、が原則です。

④ <picture> でソースごとに比率が違うとシフトする

SPとPCでトリミング違いの画像を出し分けるとき、<source> 側にも width / height を書かないと、ブレークポイントを跨いだ瞬間にレイアウトがずれます。出し分けるすべてのソースに寸法を書くか、親側を aspect-ratio でメディアクエリごとに固定してください。

<picture>
  <source media="(min-width: 768px)" srcset="hero-pc.webp" width="1600" height="900">
  <img src="hero-sp.webp" width="750" height="1000" alt="サービス紹介">
</picture>

⑤ SVGは object-fit ではなく preserveAspectRatio

インラインSVGや <svg> 要素には object-fit が効きません。比率制御は viewBox と preserveAspectRatio の担当です。<img src="icon.svg"> のように画像として読み込んでいる場合は、通常の置換要素なので object-fit が使えます。同じSVGでも埋め込み方で担当プロパティが変わる点が混乱しやすいところです。

⑥ WordPressのブロックエディタが挟む figure / div に注意

WordPressの画像ブロックは <figure class="wp-block-image"> でラップされ、テーマ側で display: flex が当たっていることがあります。子テーマ側でカードCSSを書いたのに効かない場合は、親テーマのfigureに当たっているレイアウト指定を先に確認してください。アイキャッチ(the_post_thumbnail())も同様に、テーマ既定のクラスが寸法を握っていることがあります。セレクタの詳細度で殴る前に、どの層が箱を決めているかを特定するのが結局は近道です。

3分で終わるチェックリスト

  • object-fitが効かない → その画像のボックスに高さがあるか。height: auto のままならまず aspect-ratio か height: 100% を与える
  • 縦長・横長に潰れている → 親が display: flex / grid でないか。align-self: start で直れば犯人は stretch
  • aspect-ratioが無視される → width と height が両方確定していないか。片方を auto に戻す
  • 箱からはみ出す → max-width: 100% の有無と、grid / flex アイテムの min-width: 0 / min-height: 0
  • 画像の下に数pxの隙間 → img に display: block
  • 読み込み時にガタつく → HTMLの width / height 属性が両方あるか。片方だけでは予約されない
  • 一覧全体を確認する → 上のコンソールスニペットを実行し、console.table の行がゼロになるまで潰す

画像の崩れは、「箱の大きさを誰が決めるか」と「中身をどう収めるか」を別の担当として設計していないときに起きます。この2つを分けて書く型さえ身につけてしまえば、カードでもヒーローでもサムネイルでも、同じ数行で片が付きます。逆にこの切り分けをせずに object-fit や aspect-ratio を足していくと、効かないプロパティだけが積み上がっていきます。まず箱、次に中身——順番を守るだけで、このジャンルの事故はほぼ消えます。

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

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

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

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

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

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