CSSで、料金プランのカードを横に並べてそろえる方法
料金プランのカードを並べるときは、外側を CSS Grid にし、各カードに grid-template-rows: subgrid を付けると、カードの中の行がとなりのカードとそろいます。説明文が1行のプランと4行のプランが並んでいても、機能の一覧とボタンが同じ高さから始まります。おすすめのプランに付けるバッジの置き方と、狭い画面で縦に並べる書き方もあわせて確かめました。
検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。HTML と CSS のページを Playwright で開き、getBoundingClientRect() で各要素の上端の位置(ページの上端から、px)を測りました。Firefox と Safari、スマートフォンの実機、スクリーンリーダーでの読み上げは試していません。

カードの HTML は、どのプランも同じ並びにする
カードは ul の li にし、中の要素の並びを、どのプランでも同じにします。プラン名、価格、説明文、機能の一覧、ボタンの5つです。
<ul class="plans">
<li class="plan">
<h3>フリー</h3>
<p class="price"><span class="amount">0円</span><span class="unit">/月</span></p>
<p class="desc">まずは試してみたい人に。</p>
<ul class="features">
<li>プロジェクト 1件</li>
<li>保存容量 1GB</li>
</ul>
<a class="cta" href="#">無料ではじめる</a>
</li>
<li class="plan featured">
<p class="badge">おすすめ</p>
<h3>スタンダード</h3>
…
</li>
…
</ul>
試したページでは、スタンダードの説明文だけを長くし(幅800pxの画面で4行)、機能の数もプランごとに2個、4個、3個と変えました。枠線や文字の大きさなど、見た目だけの指定は別のファイルに分けています。はじめは、スタンダードの「おすすめ」のバッジを display: none で隠して位置を測りました。この後の比べる画像と、「おすすめのバッジ」の節までの数値は、バッジを隠した状態のものです。
縦の flex でボタンを下に寄せても、一覧の位置はずれる
カードを横に並べるだけなら、display: flex でできます。ただ、カードの高さはそろっても、ボタンは中身の量に応じた位置に来ます。幅800pxの画面で測ると、ボタンの上端は左から213px、335px、240pxでした。
ボタンを下にそろえるには、カードの中も縦の flex にして、ボタンに margin-top: auto を付けます。
/* カードの中を縦の flex にして、ボタンを下に寄せる */
.plans {
display: flex;
gap: 24px;
}
.plan {
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
}
.cta {
margin-top: auto;
}
flex のアイテムに auto のマージンを付けると、余った空きがそのマージンに入ります(MDN の flex のアイテムの配置についての説明にある、auto マージンの節)。3枚ともボタンの上端は383pxになりました。
ところが、機能の一覧の上端は、フリーとプロが182px、スタンダードが250pxでした。説明文の行数が違うので、その下の一覧の始まりがずれます。カードごとに別々の flex なので、となりのカードの説明文の高さは反映されません。

subgrid で、カードの中の行をとなりとそろえる
カードの中の行をそろえるには、外側を grid にし、各カードを subgrid にします。
/* カードの中の行を、となりのカードとそろえる */
.plans {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
}
.plan {
display: grid;
grid-template-rows: subgrid;
grid-row: span 5;
row-gap: 12px;
}
grid-row: span 5 で、各カードに外側のグリッドの行を5つ使わせます。grid-template-rows: subgrid にしたカードは、その5行を自分の行として使います。行の高さは外側のグリッドが決めるので、3枚のうちいちばん高い中身に合わせてそろいます。span の数は、カードの中の要素の数(ここでは5つ)に合わせます。
幅800pxの画面で測ると、3枚とも、プラン名が49px、価格が90px、説明文が148px、機能の一覧が250px、ボタンが383pxから始まりました。説明文が短いカードは、説明文の下が空きます。
row-gap: 12px を付けているのは、subgrid には外側のグリッドの gap がそのまま引き継がれるからです。MDN の subgrid の説明にも、外側の gap が引き継がれ、subgrid 側の指定で上書きできると書かれています。row-gap を書かずに試すと、カードの中の行の間も外側と同じ24pxになり、カードの高さは428pxから476pxに伸びました。
おすすめのバッジは、position: absolute で置く
おすすめのプランに付ける「おすすめ」のバッジは、ほかのカードにはない要素です。これを普通に置くと、スタンダードのカードだけ中の要素が6つになり、行の並びが1つずつずれます。試したところ、スタンダードのプラン名は90px(ほかのカードの価格の位置)から始まり、ボタンの上端は機能の一覧と同じ312pxで、2つが重なりました。
MDN の subgrid の説明によると、subgrid にした向きには、足りない行が自動では足されません。入りきらない要素は最後の行に入ります。ボタンが一覧と重なったのはこのためです。
バッジに position: absolute を付けると、行を使わなくなります。
/* おすすめのプランを目立たせる */
.plan.featured {
position: relative;
border-color: #d85878;
box-shadow: 0 0 0 1px #d85878;
}
.badge {
position: absolute;
top: 0;
left: 50%;
translate: -50% -50%;
padding: 2px 12px;
border-radius: 999px;
background: #d85878;
color: #fff;
font-size: 13px;
font-weight: 700;
}
グリッドの中で絶対位置指定をした要素は、通常の流れから外れます。その要素はグリッドの行を使いません。ほかの要素は、その要素がないものとして自動で並びます。この CSS で測ると、スタンダードのプラン名もほかと同じ49pxから始まり、ボタンも383pxにそろいました。バッジは、カードの上端(24px)をまたいで13.5pxから36.5pxの位置にあり、横の中心はカードの中心と同じでした。left: 50% と translate の1つ目の -50% で横の中心をカードに合わせ、top: 0 と2つ目の -50% で、カードの上端をまたぐように上へずらしています。測った位置では、高さ23pxのバッジの上半分(11.5px分)がカードの外に出ていました。
枠を太くするのに、border を 2px にする方法もあります。ただ、試したところ、枠線が太くなった1px分、スタンダードのプラン名だけが50pxから始まり、ほかのカードと1pxずれました。上の CSS のように、box-shadow の4つ目の値(広がりの大きさ)で外側に1px足すと、枠が2pxに見えても、中の位置はほかのカードと同じでした。
画面が狭いときは、1列に並べ直す
3列のままでは、幅の狭い画面でカードが細くなりすぎます。grid-template-columns を repeat(auto-fit, minmax(min(100%, 200px), 1fr)) にすると、1枚あたり200px以上の幅を取れるだけ並びます。auto-fit と auto-fill の違いなど、この書き方の詳しいところは、このブログの「CSS Gridで、カードを幅に合わせて自動で並べる方法」に書いています。ここまでをまとめると、次の CSS になります。
/* 料金プランのカードを並べ、中の行をそろえる */
.plans {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
gap: 24px;
}
.plan {
display: grid;
grid-template-rows: subgrid;
grid-row: span 5; /* カードの中の要素の数 */
row-gap: 12px;
}
/* おすすめのプラン(.plan.featured と .badge)は、上と同じ */
画面の幅を変えて測った結果です。
| 画面の幅 | 並び方 | 行のそろい方 |
|---|---|---|
| 800px | 3枚が横に1列 | 3枚とも同じ高さから始まる |
| 600px | 2枚、その下に1枚 | 上の2枚どうしはそろう。下のプロは、自分の中身だけの高さ(333px) |
| 375px | 1枚ずつ縦に並ぶ | どのカードも自分の中身だけの高さ(306px、405px、333px) |
縦に並んだときも、カードどうしの間は外側の gap の24pxでした。別の段に並んだカードどうしは、行を共有しないので高さが合わされません。そのため、1列のときに短いカードの下が空くことはありませんでした。幅375pxでも、おすすめのバッジはスタンダードのカードの上端をまたいで、カードの横の中心に表示されていました。
使えるブラウザのバージョン
MDN の互換性データ(@mdn/browser-compat-data 8.1.8)で調べた、この記事の CSS で新しめの機能が使えるようになったバージョンです。
| 機能 | Chrome | Firefox | Safari |
|---|---|---|---|
grid-template-rows: subgrid |
117 | 71 | 16 |
translate プロパティ |
104 | 72 | 14.1 |
min() |
79 | 75 | 11.1 |
互換性データの上では、最後の CSS で使っている機能がすべてそろうのは、Chrome 117、Firefox 75、Safari 16 からです(Chrome と Safari は subgrid、Firefox は min() が最後に対応しました)。





