近年 @media で本当に変わったのは、比較演算子で範囲を書ける「範囲構文」(2023年3月にBaseline入り)と、scripting(2023年12月)の2つです。加えて、要素幅で分岐する コンテナクエリ @container(2023年2月)が、@media の限界を埋めるものとして加わりました。

この記事は2024年の公開後、2026年9月に見直しました。 公開当時に「導入された」と書いていた light-level メディア特性は、どのブラウザエンジンにも実装されず、仕様からも削除されています。この点を訂正したうえで、実際に使える機能に差し替えました。

  • @media (light-level: dim) は動作しません。 Media Queries Level 5 の草案に一時期あっただけで実装例が無く、prefers-contrastprefers-color-scheme と役割が重複するとして仕様から外れました
  • prefers-reduced-motion(2019年)、prefers-color-scheme(2019年)、forced-colors(2020年)は2024年の新機能ではありません

いつから使えるのかを整理する

機能 使えるようになった時期 位置づけ
prefers-color-scheme 2019年 枯れた機能
prefers-reduced-motion 2019年 枯れた機能
forced-colors 2020年 枯れた機能
コンテナクエリ @container 2023年2月 Baseline 新しい
範囲構文 (400px <= width) 2023年3月 Baseline 新しい
scripting 2023年12月 新しい
prefers-reduced-transparency Chrome 118〜(Level 5草案) 実験的
light-level 実装されず、仕様から削除 使えない

 

範囲構文:min-width / max-width を書かなくてよくなった

比較演算子でそのまま範囲を書けます。 2023年3月にSafari 16.4が対応して全ブラウザで使えるようになりました。

/* 従来 */
@media (min-width: 768px) { ... }
@media (max-width: 767.98px) { ... }
@media (min-width: 768px) and (max-width: 1023.98px) { ... }

/* 範囲構文 */
@media (width >= 768px) { ... }
@media (width < 768px) { ... }
@media (768px <= width < 1024px) { ... }

 

最大の利点は「1px問題」が消えることです。 従来の max-width は指定値を含むため、min-width: 768pxmax-width: 768px を並べると 768px ちょうどで両方が適用されます。これを避けるために 767.98px のような値を書く慣習がありましたが、範囲構文なら width < 768px と書くだけで境界が明確になります。

/* 768px ちょうどで両方適用されてしまう */
@media (max-width: 768px) { .box { color: red; } }
@media (min-width: 768px) { .box { color: blue; } }

/* 重ならない */
@media (width < 768px)  { .box { color: red; } }
@media (width >= 768px) { .box { color: blue; } }

 

heightresolution にも使えます。

@media (height < 600px) { ... }
@media (resolution >= 2x) { ... }

 

scripting:JavaScriptが動くかどうかで分岐する

<noscript> のCSS版です。JavaScriptが無効、または読み込みに失敗した環境向けのスタイルを書けます。

/* JavaScript が動く環境 */
@media (scripting: enabled) {
  .fade-in { opacity: 0; transition: opacity .4s; }
  .fade-in.is-visible { opacity: 1; }
}

/* JavaScript が無い環境では最初から見えている */
@media (scripting: none) {
  .js-only-button { display: none; }
  .no-js-message { display: block; }
}

 

これは実害のあるバグを防ぎます。 スクロールアニメーションで初期状態を opacity: 0 にしていると、JavaScriptが失敗したときに本文が読めなくなります。scripting: enabled の中に閉じ込めておけば、その事故が起きません。

意味
enabled スクリプトが使える
none スクリプトが使えない
initial-only 読み込み時のみ動く(実装例はほぼ無い)

関連する実装はIntersection Observer APIを使ったスクロールアニメーションにまとめています。

 

コンテナクエリ:画面幅ではなく親要素の幅で分岐する

@media の根本的な限界は「ビューポートの幅しか見られない」ことです。 同じカードコンポーネントを、広いメイン領域と狭いサイドバーの両方に置く場合、画面幅では判断できません。

/* 親側でコンテナを宣言する */
.card-container {
  container-type: inline-size;
  container-name: card;
}

/* 親の幅が 400px 以上なら横並びにする */
@container card (width >= 400px) {
  .card {
    display: grid;
    grid-template-columns: 120px 1fr;
    gap: 16px;
  }
}

 

これで同じCSSのまま、置かれた場所に応じてレイアウトが変わるコンポーネントになります。サイドバーに置けば縦積み、メイン領域に置けば横並びです。

注意点が2つあります。

  • container-type: inline-size を指定した要素は、その方向のサイズが子要素の内容に依存しなくなります。高さが潰れる場合は container-type: size ではなく inline-size を使っているか確認してください
  • 自分自身にはクエリを掛けられません。必ず親要素にコンテナを宣言し、子要素にスタイルを当てます

コンテナ単位も使えます。

@container card (width >= 400px) {
  .card__title {
    font-size: 5cqi; /* コンテナのインライン方向サイズの5% */
  }
}

 

@media @container
基準 ビューポート 親要素
向く用途 ページ全体のレイアウト 再利用するコンポーネント
ユーザー設定の取得 できる できない

ページの骨格は @media、部品は @containerという分担が現実的です。

 

prefers-reduced-motion:正しい書き方

2019年からある機能ですが、書き方を間違えると効きません。よく見かける次のコードには2つ問題があります。

/* 効かないことがある */
@media (prefers-reduced-motion: reduce) {
  * { animation-duration: 0s; }
}

 

  • 詳細度で負ける:後から書かれた個別セレクターに上書きされます
  • transition が止まらないanimation-duration だけではトランジションは残ります

実務では次の形が確実です。

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

 

0s ではなく 0.01ms にしているのは意図的です。 完全に0にすると animationendtransitionend イベントが発火しないことがあり、「アニメーション完了後に要素を消す」といった処理が止まります。ごく短い時間を残すことで、見た目は瞬時のまま処理の流れを保てます。

JavaScript側での判定はWeb Animations APIのanimateメソッドにまとめています。

 

forced-colors:色を指定してはいけない

ここは誤解が多い機能です。 Windowsのハイコントラストモードなど、OSが色を強制する状態を検出します。

やってはいけないのは、この中で色を指定し直すことです。

/* ユーザーが選んだ配色を上書きしてしまう */
@media (forced-colors: active) {
  body {
    background-color: black;
    color: yellow;
  }
}

 

強制カラーモードでは、ユーザーが自分に見やすい配色を選んでいます。著者が黒と黄色を指定するのは、その選択を無視することになります。そもそもこのモードでは著者の色指定の多くが上書きされるため、意図通りにもなりません。

正しい使い方は、色が失われることで壊れる部分を補うことです。

@media (forced-colors: active) {
  /* 背景色で状態を示していたボタンに、輪郭を足す */
  .button--primary {
    border: 2px solid ButtonText;
  }

  /* 背景画像で描いていたアイコンは消えるので、代替を出す */
  .icon-search::after {
    content: "検索";
  }

  /* システムカラーキーワードを使う */
  .card {
    border: 1px solid CanvasText;
    background: Canvas;
    color: CanvasText;
  }
}

 

使えるシステムカラーキーワードには Canvas(背景)、CanvasText(文字)、LinkTextButtonFaceButtonTextHighlight などがあります。具体的な色ではなく「役割」で指定するのがポイントです。

ロゴなど、色を保持しなければ意味が通らない要素には forced-color-adjust: none を使います。ただし乱用は禁物で、本当に必要な箇所だけに限定してください。

 

prefers-color-scheme と light-dark()

ダークモード対応は prefers-color-scheme が基本ですが、色ごとに light-dark() 関数で書く方法も使えるようになりました。

:root {
  /* 両方のスキームに対応することを宣言する */
  color-scheme: light dark;

  --bg:   light-dark(#ffffff, #121212);
  --text: light-dark(#333333, #f2f2f2);
  --link: light-dark(#0b5cff, #8ab4ff);
}

body {
  background: var(--bg);
  color: var(--text);
}

 

color-scheme: light dark; の宣言が必須です。 これが無いと light-dark() は常にライト側の値を返します。あわせて、この宣言はフォーム部品やスクロールバーの見た目もOS側のダークテーマに合わせてくれます。

手動でのテーマ切り替えを併用する場合は、@media[data-theme] の両方を書きます。

:root { --bg: #ffffff; --text: #333333; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) { --bg: #121212; --text: #f2f2f2; }
}

:root[data-theme="dark"] { --bg: #121212; --text: #f2f2f2; }

 

色の設計自体には oklch() が便利です。詳しくはCSS color()関数の使い方と実用例にまとめました。

 

知っておくと役立つメディア特性

特性 用途
(hover: hover) ホバーできる環境だけにホバー効果を付ける
(pointer: coarse) 指で操作する環境。タップ領域を広げる
(prefers-contrast: more) 高コントラストを希望している
(prefers-reduced-transparency: reduce) 半透明を減らしたい(Chrome 118〜)
(update: slow) 電子ペーパーなど。アニメーションを止める
(display-mode: standalone) PWAとしてインストールされた状態

特に (hover: hover) は実用性が高いです。タッチデバイスでホバー効果を書くと、タップ後にスタイルが残り続けるという定番の不具合を防げます。

@media (hover: hover) and (pointer: fine) {
  .card:hover {
    transform: translateY(-4px);
    box-shadow: 0 8px 24px rgb(0 0 0 / .12);
  }
}

 

まとめ

  • light-level は実装されず仕様からも削除されました。 使えません
  • prefers-color-scheme / prefers-reduced-motion / forced-colors は2019〜2020年からの機能
  • 範囲構文 (768px <= width < 1024px) で「1px問題」が消える
  • scripting: enabled でJavaScript失敗時にコンテンツが消える事故を防げる
  • コンポーネント単位の分岐は @media ではなく @container
  • prefers-reduced-motion!important 付きで transition も止める。0sではなく0.01ms
  • forced-colors の中で色を指定しない。 輪郭やテキスト代替を足す方向で対応する
  • light-dark() を使うなら color-scheme: light dark; の宣言が必須

メディアクエリの記事は情報が古いまま残りやすい分野です。「その特性を実装しているブラウザが実在するか」を Can I use などで確認してから使ってください。

ABOUT ME
りん
このブログでは、Web開発やプログラミングに関する情報を中心に、私が日々感じたことや学んだことをシェアしています。技術と生活の両方を楽しめるブログを目指して、日常で触れた出来事や本、グルメの話題も取り入れています。気軽に覗いて、少しでも役立つ情報や楽しいひとときを見つけてもらえたら嬉しいです。