Container Queries 適応カード

container-type と @container で、置かれたコンテナの幅に応じて同じカードが縦積み↔横並びへ切り替わる。スライダーで挙動を体感できる。

#css#container-queries#responsive

ライブデモ

使用例(お題: カフェ MOON BREW)

この技法を「カフェ MOON BREW」というテーマのダミーサイトで実際に使った例です。

HTML
<!-- MOON BREW メニュー:コンテナ幅でメニューカードが縦積み↔横並びに切替 -->
<section class="mb-cq">
  <header class="mb-cq__bar">
    <h2 class="mb-cq__title">☕ 本日のメニュー</h2>
    <label class="mb-cq__ctrl">
      陳列幅
      <input id="mbWidth" type="range" min="220" max="560" value="540">
    </label>
  </header>

  <div class="mb-cq__stage">
    <!-- このラッパが container。幅に応じて中のカードが変形する -->
    <div class="mb-cq__container" id="mbContainer">
      <article class="mb-cq__card">
        <div class="mb-cq__photo" aria-hidden="true"></div>
        <div class="mb-cq__body">
          <div class="mb-cq__head">
            <h3 class="mb-cq__name">琥珀ラテ</h3>
            <span class="mb-cq__price">¥580</span>
          </div>
          <p class="mb-cq__desc">深煎り豆にキャラメルを一垂らし。ふんわりミルクで包んだ看板の一杯。</p>
          <span class="mb-cq__tag">人気No.1</span>
        </div>
      </article>
    </div>
  </div>

  <p class="mb-cq__hint" id="mbHint">幅を縮めると、写真の上に文字が乗る縦型カードに切り替わります。</p>
</section>
CSS
/* MOON BREW:container-type と @container でメニューカードを適応させる */
:root {
  --cream: #f5ede1;
  --brown: #2b1d12;
  --amber: #c98a3b;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  height: 400px;
  display: grid;
  place-items: center;
  font-family: "Hiragino Kaku Gothic ProN", "Segoe UI", system-ui, sans-serif;
  background: var(--cream);
  color: var(--brown);
  overflow: hidden;
}

.mb-cq {
  width: 100%;
  max-width: 600px;
  padding: 18px;
}

.mb-cq__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 16px;
}
.mb-cq__title { margin: 0; font-size: 17px; font-weight: 800; }
.mb-cq__ctrl {
  font-size: 11px;
  color: #6b5640;
  display: flex;
  align-items: center;
  gap: 8px;
}
.mb-cq__ctrl input { accent-color: var(--amber); cursor: pointer; }

/* ステージ:ここでカードの陳列幅を変える */
.mb-cq__stage {
  display: flex;
  justify-content: center;
  padding: 14px;
  border-radius: 14px;
  background: #fff;
  border: 1px dashed #e0cfb3;
}

/* container 化したラッパ。幅をクエリ対象にする */
.mb-cq__container {
  container-type: inline-size;
  width: 540px;
  max-width: 100%;
}

.mb-cq__card {
  display: flex;
  gap: 0;
  border-radius: 14px;
  overflow: hidden;
  background: #fffaf2;
  border: 1px solid #ecdcc1;
  box-shadow: 0 8px 20px rgba(43, 29, 18, 0.12);
}
.mb-cq__photo {
  flex: 0 0 44%;
  min-height: 150px;
  background: url("https://picsum.photos/400/300?random=42") center/cover no-repeat;
  filter: sepia(0.2) saturate(1.1);
}
.mb-cq__body { flex: 1; padding: 16px 18px; }
.mb-cq__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}
.mb-cq__name { margin: 0; font-size: 18px; font-weight: 800; }
.mb-cq__price { font-size: 17px; font-weight: 800; color: var(--amber); }
.mb-cq__desc { margin: 8px 0 12px; font-size: 12px; line-height: 1.7; color: #6b5640; }
.mb-cq__tag {
  display: inline-block;
  font-size: 10px;
  font-weight: 700;
  padding: 4px 10px;
  border-radius: 999px;
  background: var(--brown);
  color: var(--cream);
  letter-spacing: 0.08em;
}

/* コンテナが狭いとき:写真の上に文字を乗せた縦型カードへ */
@container (max-width: 360px) {
  .mb-cq__card { flex-direction: column; }
  .mb-cq__photo {
    flex: none;
    min-height: 120px;
  }
  .mb-cq__body { padding: 14px 16px 16px; }
  .mb-cq__name { font-size: 16px; }
}

.mb-cq__hint {
  margin: 14px 2px 0;
  font-size: 11px;
  color: #8a7459;
  text-align: center;
  line-height: 1.6;
}
JavaScript
// スライダーでコンテナ幅を変え、Container Queries の切替を体感させる
const slider = document.getElementById("mbWidth");
const container = document.getElementById("mbContainer");
const hint = document.getElementById("mbHint");

if (slider && container) {
  const apply = () => {
    const w = Number(slider.value);
    container.style.width = w + "px";
    // 360px を境にレイアウトが切り替わるので、状態を案内文に反映
    if (hint) {
      hint.textContent = w <= 360
        ? "現在は縦型カード(@container max-width:360px が適用中)。"
        : "幅を縮めると、写真の上に文字が乗る縦型カードに切り替わります。";
    }
  };
  slider.addEventListener("input", apply);
  apply(); // 初期反映
}

コード

HTML
<!-- Container Queries 適応カード:親コンテナ幅で同じカードが縦↔横レイアウトに切替 -->
<div class="cq">
  <div class="cq__bar">
    <span class="cq__hint">スライダーでコンテナ幅を変える →</span>
    <input id="cqRange" class="cq__range" type="range" min="200" max="640" value="560"
           aria-label="コンテナ幅">
    <span class="cq__val" id="cqVal">560px</span>
  </div>

  <!-- このラッパが container。幅に応じて中のカードが変化 -->
  <div class="cq__wrap" id="cqWrap">
    <article class="ccard">
      <div class="ccard__media" aria-hidden="true">
        <span class="ccard__emoji">🪴</span>
      </div>
      <div class="ccard__body">
        <span class="ccard__tag">CONTAINER QUERY</span>
        <h2 class="ccard__title">幅で姿を変えるカード</h2>
        <p class="ccard__text">
          コンテナが広いと横並び、狭いと縦積み。メディアクエリと違い
          “置かれた場所”の幅で判断するのが強みです。
        </p>
        <button class="ccard__btn">詳しく見る</button>
      </div>
    </article>
  </div>
</div>
CSS
/* 全体:暖色グラデのステージ */
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
  font-family: "Hiragino Sans", system-ui, sans-serif;
  background: linear-gradient(160deg, #fff7ed, #ffe4d6 60%, #ffd9c2);
  color: #3a2418; min-height: 100vh; padding: 16px;
  display: flex; flex-direction: column; align-items: center; gap: 14px;
}

/* 操作バー */
.cq { width: min(720px, 100%); display: flex; flex-direction: column; gap: 14px; align-items: center; }
.cq__bar {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap; justify-content: center;
  background: #fffaf6; border: 1px solid #f3d6c4; border-radius: 12px; padding: 10px 14px;
  box-shadow: 0 10px 24px -18px rgba(120,60,20,.5);
}
.cq__hint { font-size: 12px; color: #9a6a4d; }
.cq__range { accent-color: #f97316; width: 180px; }
.cq__val { font-size: 12px; font-weight: 800; color: #ea580c; min-width: 56px; text-align: right; }

/* container 指定:このラッパがクエリ対象 */
.cq__wrap {
  container-type: inline-size;
  width: 560px; max-width: 100%;
  transition: width .15s ease;
}

/* カード本体(デフォルト=狭い:縦積み) */
.ccard {
  display: flex; flex-direction: column; overflow: hidden;
  background: #ffffff; border: 1px solid #f3d6c4; border-radius: 18px;
  box-shadow: 0 20px 44px -24px rgba(120,60,20,.55);
}
.ccard__media {
  display: grid; place-items: center; min-height: 120px;
  background: linear-gradient(135deg, #fb923c, #f97316 60%, #ef4444);
}
.ccard__emoji { font-size: 52px; filter: drop-shadow(0 6px 10px rgba(0,0,0,.25)); }
.ccard__body { padding: 18px; display: flex; flex-direction: column; gap: 9px; }
.ccard__tag {
  align-self: flex-start; font-size: 10px; letter-spacing: .18em; font-weight: 800;
  color: #c2410c; background: #ffedd5; padding: 4px 10px; border-radius: 20px;
}
.ccard__title { font-size: 18px; font-weight: 800; color: #431407; line-height: 1.3; }
.ccard__text { font-size: 13px; line-height: 1.7; color: #7c5440; }
.ccard__btn {
  align-self: flex-start; margin-top: 4px;
  font: inherit; font-size: 13px; font-weight: 700; cursor: pointer;
  color: #fff; border: none; border-radius: 10px; padding: 9px 18px;
  background: linear-gradient(90deg, #fb923c, #ef4444);
  transition: transform .1s, filter .2s;
}
.ccard__btn:hover { filter: brightness(1.06); }
.ccard__btn:active { transform: scale(.97); }

/* コンテナが 420px 以上なら横並びレイアウトへ */
@container (min-width: 420px) {
  .ccard { flex-direction: row; align-items: stretch; }
  .ccard__media { min-height: auto; flex: 0 0 42%; }
  .ccard__body { flex: 1; justify-content: center; }
  .ccard__title { font-size: 21px; }
}

@media (prefers-reduced-motion: reduce) {
  .cq__wrap, .ccard__btn { transition: none; }
}
JavaScript
// スライダーでコンテナ幅を変え、Container Query の切替を体感させる
(() => {
  const range = document.getElementById('cqRange');
  const wrap = document.getElementById('cqWrap');
  const val = document.getElementById('cqVal');
  if (!range || !wrap || !val) return;

  const apply = () => {
    const w = range.value;
    wrap.style.width = w + 'px';
    val.textContent = w + 'px';
  };
  range.addEventListener('input', apply);
  apply(); // 初期反映
})();

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「Container Queries 適応カード」の効果を追加してください。

# 追加してほしい効果
Container Queries 適応カード(レイアウト & グリッド)
container-type と @container で、置かれたコンテナの幅に応じて同じカードが縦積み↔横並びへ切り替わる。スライダーで挙動を体感できる。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- Container Queries 適応カード:親コンテナ幅で同じカードが縦↔横レイアウトに切替 -->
<div class="cq">
  <div class="cq__bar">
    <span class="cq__hint">スライダーでコンテナ幅を変える →</span>
    <input id="cqRange" class="cq__range" type="range" min="200" max="640" value="560"
           aria-label="コンテナ幅">
    <span class="cq__val" id="cqVal">560px</span>
  </div>

  <!-- このラッパが container。幅に応じて中のカードが変化 -->
  <div class="cq__wrap" id="cqWrap">
    <article class="ccard">
      <div class="ccard__media" aria-hidden="true">
        <span class="ccard__emoji">🪴</span>
      </div>
      <div class="ccard__body">
        <span class="ccard__tag">CONTAINER QUERY</span>
        <h2 class="ccard__title">幅で姿を変えるカード</h2>
        <p class="ccard__text">
          コンテナが広いと横並び、狭いと縦積み。メディアクエリと違い
          “置かれた場所”の幅で判断するのが強みです。
        </p>
        <button class="ccard__btn">詳しく見る</button>
      </div>
    </article>
  </div>
</div>

【CSS】
/* 全体:暖色グラデのステージ */
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
  font-family: "Hiragino Sans", system-ui, sans-serif;
  background: linear-gradient(160deg, #fff7ed, #ffe4d6 60%, #ffd9c2);
  color: #3a2418; min-height: 100vh; padding: 16px;
  display: flex; flex-direction: column; align-items: center; gap: 14px;
}

/* 操作バー */
.cq { width: min(720px, 100%); display: flex; flex-direction: column; gap: 14px; align-items: center; }
.cq__bar {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap; justify-content: center;
  background: #fffaf6; border: 1px solid #f3d6c4; border-radius: 12px; padding: 10px 14px;
  box-shadow: 0 10px 24px -18px rgba(120,60,20,.5);
}
.cq__hint { font-size: 12px; color: #9a6a4d; }
.cq__range { accent-color: #f97316; width: 180px; }
.cq__val { font-size: 12px; font-weight: 800; color: #ea580c; min-width: 56px; text-align: right; }

/* container 指定:このラッパがクエリ対象 */
.cq__wrap {
  container-type: inline-size;
  width: 560px; max-width: 100%;
  transition: width .15s ease;
}

/* カード本体(デフォルト=狭い:縦積み) */
.ccard {
  display: flex; flex-direction: column; overflow: hidden;
  background: #ffffff; border: 1px solid #f3d6c4; border-radius: 18px;
  box-shadow: 0 20px 44px -24px rgba(120,60,20,.55);
}
.ccard__media {
  display: grid; place-items: center; min-height: 120px;
  background: linear-gradient(135deg, #fb923c, #f97316 60%, #ef4444);
}
.ccard__emoji { font-size: 52px; filter: drop-shadow(0 6px 10px rgba(0,0,0,.25)); }
.ccard__body { padding: 18px; display: flex; flex-direction: column; gap: 9px; }
.ccard__tag {
  align-self: flex-start; font-size: 10px; letter-spacing: .18em; font-weight: 800;
  color: #c2410c; background: #ffedd5; padding: 4px 10px; border-radius: 20px;
}
.ccard__title { font-size: 18px; font-weight: 800; color: #431407; line-height: 1.3; }
.ccard__text { font-size: 13px; line-height: 1.7; color: #7c5440; }
.ccard__btn {
  align-self: flex-start; margin-top: 4px;
  font: inherit; font-size: 13px; font-weight: 700; cursor: pointer;
  color: #fff; border: none; border-radius: 10px; padding: 9px 18px;
  background: linear-gradient(90deg, #fb923c, #ef4444);
  transition: transform .1s, filter .2s;
}
.ccard__btn:hover { filter: brightness(1.06); }
.ccard__btn:active { transform: scale(.97); }

/* コンテナが 420px 以上なら横並びレイアウトへ */
@container (min-width: 420px) {
  .ccard { flex-direction: row; align-items: stretch; }
  .ccard__media { min-height: auto; flex: 0 0 42%; }
  .ccard__body { flex: 1; justify-content: center; }
  .ccard__title { font-size: 21px; }
}

@media (prefers-reduced-motion: reduce) {
  .cq__wrap, .ccard__btn { transition: none; }
}

【JavaScript】
// スライダーでコンテナ幅を変え、Container Query の切替を体感させる
(() => {
  const range = document.getElementById('cqRange');
  const wrap = document.getElementById('cqWrap');
  const val = document.getElementById('cqVal');
  if (!range || !wrap || !val) return;

  const apply = () => {
    const w = range.value;
    wrap.style.width = w + 'px';
    val.textContent = w + 'px';
  };
  range.addEventListener('input', apply);
  apply(); // 初期反映
})();

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。