ReactでCSSのコンテナクエリを使い、置き場所で形を変える方法
CSS のコンテナクエリを使うと、React のコンポーネントの形を、画面の幅ではなく、置かれた場所の幅で切り替えられます。同じ商品カードを、広い列では横並びに、狭い場所では縦並びにできます。幅を測る JavaScript は要りません。置き場所の要素に container-type を付け、カードの CSS に @container で条件を書きます。
検証環境は Ubuntu 24.04、Node.js 22.22.0 です。使ったのは react / react-dom 19.3.1、@types/react 19.3.0(型の定義は csstype 3.2.3)、typescript 7.0.2、esbuild 0.28.2 です。.tsx を tsc で型チェックしてから esbuild で開発用に変換し、Chrome for Testing 153(Chromium)を Playwright 1.64.1 で操作して、計算後のスタイルを読みました。Firefox や Safari、スマホの実機では試していません。
置き場所になる要素と、カードの CSS
まず、カードを置く場所になるコンポーネント CardSlot と、カード本体の ProductCard を作ります。React の側は、クラス名を付けるだけです。
import type { ReactNode } from 'react';
import './ProductCard.css';
type ProductCardProps = {
name: string;
price: number;
description: string;
};
export function ProductCard({ name, price, description }: ProductCardProps) {
return (
<article className="product-card">
<div className="product-card__image" />
<div>
<h3 className="product-card__title">{name}</h3>
<p className="product-card__price">{price.toLocaleString('ja-JP')}円</p>
<p className="product-card__desc">{description}</p>
</div>
</article>
);
}
// カードを置く場所。この要素の幅でカードの形が決まる
export function CardSlot({ children }: { children: ReactNode }) {
return <div className="card-slot">{children}</div>;
}
CSS では、.card-slot に container-type: inline-size を付けて、@container の条件で幅を調べる対象(コンテナ)にします。container-name で名前も付けておきます。カードは、狭いときの形を基本に書き、@container card-slot (width >= 400px) の中に、広いときの形を書きます。
/* 置き場所になる要素。@container の条件では、この要素の幅を調べる */
.card-slot {
container-type: inline-size;
container-name: card-slot;
}
/* 狭いときの形(基本) */
.product-card {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
padding: 16px;
border: 2px solid #00c8e8;
border-radius: 10px;
background: #eefafd;
color: #1f2a30;
}
.product-card__image {
aspect-ratio: 16 / 9;
border-radius: 8px;
background: #00c8e8;
}
.product-card__title {
margin: 0;
font-size: clamp(16px, 5cqi, 26px);
}
.product-card__desc {
display: none;
margin: 0;
}
/* 置き場所の幅が 400px 以上のときの形 */
@container card-slot (width >= 400px) {
.product-card {
grid-template-columns: 140px 1fr;
align-items: center;
}
.product-card__desc {
display: block;
}
}
MDN のコンテナクエリのガイドによると、inline-size は、コンテナのインライン方向(横書きなら横)の大きさで条件を判定する値です。見出しの 5cqi は、コンテナのインライン方向の大きさの 5% という意味の単位です。
使う側では、CardSlot で包んで置くだけです。カードは、自分がどこに置かれたかを props で受け取りません。検証に使った画面から、2つの置き場所の部分を抜き出すと次のとおりです。
<div style={{ width: 560 }}>
<CardSlot><ProductCard {...item} /></CardSlot>
</div>
<div style={{ width: 260 }}>
<CardSlot><ProductCard {...item} /></CardSlot>
</div>

ウィンドウの幅は 900px で同じまま、2つの場所で計算後のスタイルを読んだ結果です。
幅 560px の場所: {"slotWidth":560,"columns":"140px 372px","desc":"block","descInDom":1,"titleFont":"26px"}
幅 260px の場所: {"slotWidth":260,"columns":"224px","desc":"none","descInDom":1,"titleFont":"16px"}
幅 560px の場所ではグリッドが2列になり、説明が出ました。幅 260px の場所では1列になり、説明は隠れました。見出しの大きさは、560px の 5% の 28px が上限の 26px に、260px の 5% の 13px が下限の 16px に収まっています。
区切りの前後と、幅を変えたとき
置き場所の幅を state にして、400px と 399px で比べました。
置き場所の幅 400px: {"slotWidth":400,"columns":"140px 212px","desc":"block","descInDom":1,"titleFont":"20px"}
置き場所の幅 399px: {"slotWidth":399,"columns":"363px","desc":"none","descInDom":1,"titleFont":"19.95px"}
width >= 400px と書いたので、400px ちょうどで横並びになりました。幅が変わると、ブラウザが条件を判定し直します。MDN の @container のページにも、コンテナの大きさが変わったときに条件を判定すると書かれています。ProductCard は、幅が変わったことを props などで受け取らないままです。

コンテナにした要素自身には、@container のスタイルが当たらない
container-type を付けた要素そのものに、@container の中でスタイルを当てても効きません。幅 560px の要素に container-type: inline-size を付け、@container (width >= 400px) の中で、その要素の背景と、子の文字の色をピンクにしました。
{"width":560,"selfBackground":"rgb(255, 255, 255)","childColor":"rgb(216, 88, 120)"}
子の文字はピンクになりましたが、要素自身の背景は白のままでした。MDN の @container のページにも、条件が合ったときにスタイルが当たるのは、コンテナの子孫の要素だと書かれています。カードの枠ごと形を変えたいので、この記事では CardSlot という外側の要素を、別に用意しています。
flex の中に置くと、幅が 0 になる
CardSlot を、幅 600px の display: flex の行に、幅 100px の要素と並べて置きました。CardSlot には幅も flex も指定していません。2つ目の行だけ、CardSlot に flex: 1 を付けています。
row-plain: {"slotWidth":0,"columns":"42.9688px","desc":"none","descInDom":1,"titleFont":"16px"}
row-grow: {"slotWidth":492,"columns":"140px 304px","desc":"block","descInDom":1,"titleFont":"24.6px"}
幅を指定しなかった行では、置き場所の幅が 0 になり、カードは縦並びのままでした。グリッドの列の幅(42.9688px)は、幅 0 の置き場所からはみ出しています。flex: 1 を付けた行では、残りの 492px に広がり、横並びになりました。
container-type: inline-size を付けた要素は、中身から横幅を決めなくなります。MDN の container-type のページによると、コンテナの大きさは、親いっぱいに広がるブロック要素のように周りから決まるか、はっきり指定する必要があり、どちらもないと潰れます。flex の子にするときのように、周りから幅が決まらない置き方では、幅か flex: 1 などを付けてください。
同じページには、こうしている理由も書かれています。中身から大きさが決まると、クエリで当てたスタイルが中身の大きさを変え、それがまたクエリの結果を変える、という繰り返しが起きうるためです。
入れ子のときは、判定に使うコンテナを名前で決める
名前を付けない @container (width >= 400px) は、一番近い祖先のコンテナの大きさで判定します。幅 600px の外側のコンテナ(名前 page)の中に、幅 250px の内側のコンテナを置き、その中の文字の色で比べました。
{"outer":600,"inner":250,"nearest":"rgb(0, 0, 0)","named":"rgb(216, 88, 120)"}
名前なしの条件は、近いほうの 250px で判定されて合わず、黒のままでした。@container page (width >= 400px) と名前を付けた条件は、外側の 600px で判定され、ピンクになりました。MDN の @container のページの説明どおりです。カードの中に、さらにコンテナを持つ部品を入れることもあるので、カードの CSS では、card-slot の名前を条件に付けて、判定に使うコンテナを決めています。
style 属性に書くときは、値の書き間違いに注意する
container-type と container-name は、React の style 属性にも書けます。ただし、MDN の style 属性のページにあるとおり、style 属性に入るのは CSS の宣言です。@container の条件は、CSS ファイルに書きます。
<div style={{ width: 560, containerType: 'inline-size', containerName: 'card-slot' }}>
<ProductCard {...item} />
</div>
<div style={{ width: 560, containerType: 'inline', containerName: 'card-slot' }}>
<ProductCard {...item} />
</div>
2つ目は inline-size を inline と書き間違えたものですが、tsc の型チェックは通りました。@types/react が使う csstype の型では、containerType が決まった値のほかに、任意の文字列も受け付けるためです。
export type ContainerType = Globals | "inline-size" | "normal" | "scroll-state" | "size" | (string & {});
ブラウザで開くと、次のようになりました。
ok: {"styleAttr":"width: 560px; container: card-slot / inline-size;","containerType":"inline-size","columns":"140px 372px"}
typo: {"styleAttr":"width: 560px; container-name: card-slot;","containerType":"normal","columns":"524px"}
正しく書いたほうは横並びになりました。書き間違えたほうは、container-type が style 属性に残らず、既定の normal のままでした。コンテナにならないので、幅 560px あってもカードは縦並びのままです。ページのエラーも出ませんでした。置き場所のコンポーネントを作るなら、CardSlot のように CSS のクラスで付けておくほうが、書き間違えにくくなります。
隠した要素も、React は描画している
幅 260px の場所のカードでも、説明の p 要素は DOM にありました(descInDom が 1)。コンテナクエリが変えるのは CSS だけで、display: none で隠れていても、React はその要素を作っています。隠す部分が重い処理を含むときや、出す部品そのものを変えたいときは、JavaScript で置き場所の幅を受け取る方法(ResizeObserver など)を使います。
置き場所になる外側の要素に container-type: inline-size を付け、部品の CSS に @container 名前 (width >= 400px) の形で条件を書きます。外側の要素は、親から幅が決まる置き方にします。React のコンポーネントは、置き場所を props で受け取らなくても、置かれた場所の幅で形が変わります。





