CSSメディアクエリのrange構文|比較演算子の書き方と境界値の罠


CSSメディアクエリのrange構文とは、@media (width >= 768px) のように比較演算子(< <= > >= =)で条件を書くMedia Queries Level 4の記法です。2025年9月27日に Baseline widely available へ到達しているため、2026年現在はフォールバックを用意せずそのまま使えます。

この記事は「新しい書き方の紹介」ではなく、境界値でどう動くかを実測で確定させたリファレンスです。min-width: 1201pxwidth > 1200px は等価ではないこと、ちょうど768pxのときにどちらのブロックが当たるのか、書いたつもりで黙って無効になっている条件をどう見つけるのか。掲載しているコードと数値はすべて Chrome 150(macOS・Playwright)で実行して確認しています(測定日: 2026年8月2日)。


CSSメディアクエリのrange構文とは

range構文は、メディア特性のうち「範囲」を持つもの(widthheightresolutionaspect-ratio など)に対して比較演算子を直接書ける文法です。W3C の Media Queries Level 4 が <mf-range> として定義しています(CSS Media Queries Level 4: Range Context/2026年8月2日閲覧)。

メディアタイプ(screen / print)や指定できるメディア特性の一覧といった、メディアクエリそのものの基本構文はメディアクエリの基本と使い方で解説しています。本記事はそこから先の、range構文だけを扱います。

比較演算子・チェーン比較・論理演算子は別の階層の話

range構文まわりで混乱が起きるのは、性質の違う3つを同じ表に並べてしまうからです。仕様上はっきり別物なので、最初に分けて把握しておくと以降が楽になります。

階層役割
比較演算子1つのメディア特性を1つの値と比べる@media (width >= 768px)
チェーン比較1つのメディア特性を上下から挟んで範囲にする@media (600px <= width <= 1200px)
論理演算子独立した複数の条件をつなぐ@media (width >= 768px) and (hover: hover)

つまり @media (600px <= width <= 1200px) は「and の別表記」ではありません。これは1つのメディア特性に対する範囲指定であり、and は「幅」と「ホバー可否」のように異なる特性どうしをつなぐためのものです。両者は仕様でも別の文法として定義されています。

比較演算子は5種類です。= を含めて5つあることは意外と知られていません。

演算子意味旧記法での等価表現
<未満@media (width < 600px)なし(599.98px などで近似するしかない)
<=以下@media (width <= 600px)@media (max-width: 600px)
>より大きい@media (width > 1200px)なし
>=以上@media (width >= 1200px)@media (min-width: 1200px)
=ちょうど一致@media (width = 768px)なし

= は完全一致なので実務ではほとんど使いません。Chrome 150 で実測したところ、ビューポート幅768pxでは matchMedia('(width = 768px)')true、767pxでも768.5pxでも false でした。ブラウザのズームや高DPI環境では幅が小数になるため、= で狙った条件は簡単に外れます。

min-width / max-width との正しい対応関係

旧記法との対応は仕様が明文化しています。W3C Media Queries Level 4 の 2.4.4 節に、次のとおり書かれています(W3C: Media Queries Level 4/2026年8月2日閲覧)。

Using a “min-” prefix on a feature name is equivalent to using the “>=” operator. […] Using a “max-” prefix on a feature name is equivalent to using the “<=” operator.(特性名に “min-” を付けることは “>=” 演算子を使うことと等価であり、“max-” を付けることは “<=” 演算子を使うことと等価である)

ここで押さえるべきは、等価なのは「同じ数値どうし」の対応だけだという点です。min-width: 1200pxwidth >= 1200px は完全に同じ意味ですが、min-width: 1201pxwidth > 1200px は同じではありません。この1pxずらしが実際に何を壊すのかは、後段で実測とともに示します。

なお width は「range 型」のメディア特性であるため比較演算子を受け付けます。orientationhover のような「discrete 型」の特性には比較演算子を使えません(MDN: @media / width/2026年8月2日閲覧)。


いまrange構文を使ってよいのか(Baselineで判断する)

結論として、業務案件で使って構いません。range構文は 2023年3月27日に Baseline newly available、2025年9月27日に Baseline widely available へ到達済みです(webstatus.dev: Media query range syntax/2026年8月2日取得)。Baseline widely available は「すべてのコアブラウザで使えるようになった日から30か月が経過した」状態を指し、実務上フォールバックを前提にしなくてよい水準です(web.dev: Baseline/2026年8月2日閲覧)。

ブラウザ別の初対応バージョンは次のとおりです。値は MDN のブラウザ互換データ(BCD)の css.at-rules.media.range_syntax を、web-features のデータセット経由で取得したものです(2026年8月2日取得)。

ブラウザ初対応バージョンリリース時期の目安
Chrome1042022年8月
Edge1042022年8月
Firefox1022022年6月
Safari16.42023年3月
Safari(iOS)16.42023年3月
Chrome(Android)1042022年8月

全モダンブラウザが揃ったのは Safari 16.4 がリリースされた2023年3月です。「2022年に登場した新記法」という説明を見かけますが、Chrome と Firefox が対応した年を指しているだけで、実務で使えるようになったのは2023年3月、フォールバック不要と言い切れるようになったのは2025年9月です。

ひとつ注意点があります。Can I use を参照すると Firefox の初対応が 63 と表示されます(2026年8月2日取得)。一方 BCD は 102 です。40バージョンぶんの開きがあるため、どちらを採るかで「対応済み」と判断できる範囲が変わります。本記事は BCD の102を採用しています。MDN の互換表も webstatus.dev の Baseline 判定も同じ BCD を基盤にしており、参照先どうしで数字が食い違わないためです。

なお、画面幅ではなく親要素の幅で切り替えたい場合はメディアクエリではなくコンテナクエリの領分です。使い分けはCSSコンテナクエリ完全ガイドにまとめています。


旧記法の「1pxずらし」がrange構文で消える

range構文の実利は「読みやすい」ことではありません。旧記法では書けなかった条件が書けるようになることです。その代表が、1200pxを境に切り替えるという何でもないレイアウトです。

なぜ min-width: 1201px と書く羽目になっていたのか

旧記法には <(未満)と >(より大きい)に相当する書き方がありません。使えるのは min-(以上)と max-(以下)だけです。そのため「1200px以下」と「1200pxより大きい」を排他的に分けようとすると、片方を1pxずらすしかありませんでした。

/* 旧記法:1pxずらして排他にしたつもり */
#box { background: white; }

@media (max-width: 1200px) {
  #box { background: lightgreen; }
}

@media (min-width: 1201px) {
  #box { background: lightblue; }
}

整数幅で見るかぎり、このコードは正しく動きます。1200pxでは緑、1201pxでは青になります。問題は、ビューポート幅が必ず整数になるとはかぎらないことです。

小数幅では1pxずらしが「穴」になる(実測)

ブラウザのズーム倍率が100%以外のときや、一部の高DPI環境では、CSSピクセルでのビューポート幅が小数になります。上のコードを幅1200.5pxで描画すると、次のようになりました。

ビューポート幅旧記法(max-width:1200px / min-width:1201px)range構文(width <= 1200px / width > 1200px)
1200px
1200.5px白(どちらのブロックも当たらない)
1201px

1200.5pxでは max-width: 1200px にも min-width: 1201px にもマッチせず、幅0.5pxぶんだけ、どのブレークポイントも適用されない領域が生まれます。ベーススタイルに素通りするため、この幅ではスマホ用でもPC用でもないレイアウトが表示されます。ウィンドウをドラッグしてリサイズしたときや、ブラウザのズームを100%以外にしたときに、境界付近でだけレイアウトが崩れて見える場合は、この穴を疑ってください。

個々の条件の評価結果も測っています。幅1200.5pxのときの matchMedia() の返り値は次のとおりでした。

条件幅1200.5pxでの結果
(width > 1200px)true
(min-width: 1201px)false
(width >= 1200px)true
(min-width: 1200px)true
(width <= 1200px)false
(max-width: 1200px)false

width >= 1200pxmin-width: 1200px は同じ結果になっており、仕様どおり等価です。等価でないのは width > 1200pxmin-width: 1201px のほう、つまり1pxずらしで代用していた組み合わせだけです。range構文で書けば、この代用そのものが不要になります。

/* range構文:境界に穴が空かない */
#box { background: white; }

@media (width <= 1200px) {
  #box { background: lightgreen; }
}

@media (width > 1200px) {
  #box { background: lightblue; }
}

「min-widthの単方向で書く」という設計原則とは矛盾しない

モバイルファーストの定石として「上書きの向きを一方向に固定し、min-width だけで積み上げる」と教わった方は、ここで混乱するかもしれません。結論を言うと、両者は矛盾しません。上書きの向きを一方向に固定するという設計原則と、その条件をどの記法で書くかは別のレイヤーの話だからです。

モバイルファーストをrange構文で書くなら、次のように width >= だけを並べれば済みます。単方向であることは何も変わりません。

/* ベースはSP。以降は広い側へ一方向に積み上げる */
.grid { display: grid; grid-template-columns: 1fr; }

@media (width >= 768px) {
  .grid { grid-template-columns: repeat(2, 1fr); }
}

@media (width >= 1024px) {
  .grid { grid-template-columns: repeat(3, 1fr); }
}

単方向で積み上げるかぎり、そもそも境界に穴も重複も生まれません。1pxずらしが必要になるのは、max-min- を向かい合わせて排他的に分割しようとしたときだけです。ブレークポイントをどの値に置くか、なぜ端末名ではなく「レイアウトが崩れる地点」で決めるのかといった設計側の判断はレスポンシブデザイン入門で扱っています。


コピペで動くSP・TAB・PCの3段レスポンシブ

range構文の挙動を目で確かめるための最小コードです。背景色が切り替わるだけなので、ウィンドウをリサイズすればどの条件が効いているかがそのまま分かります。

そのまま貼れるCSS

body {
  background-color: white;
}

/* sp */
@media (width < 600px) {
  body { background-color: lightcoral; }
}

/* tab */
@media (600px <= width <= 1200px) {
  body { background-color: lightgreen; }
}

/* pc */
@media (width > 1200px) {
  body { background-color: lightblue; }
}

Chrome 150 で幅を変えて実測した結果は、500pxで lightcoral、800pxで lightgreen、1300pxで lightblue でした。スタイルシートに登録されたルール数は4(ベースの body@media 3本)で、3本すべてが CSSMediaRule として正しく存在しています。

3段の境界に穴も重複もありません。< 600px600px <= … <= 1200px> 1200px という組み合わせが、実数上のあらゆる幅をちょうど1回ずつカバーしているためです。旧記法でこれを実現しようとすると、max-width: 599pxmin-width: 1201px のような1pxずらしが2箇所発生し、小数幅で2つの穴が空きます。

CSSに // でコメントを書くと @media が丸ごと消える

上のコードで /* sp */ と書いている箇所を // sp にしてはいけません。// はCSSのコメント構文ではないからです。 CSSのコメントは /* */ のみです(MDN: CSS のコメント/2026年8月2日閲覧)。// が使えるのはSass/SCSSであり、コンパイル時に取り除かれる前提の記法です(Sass: Comments/2026年8月2日閲覧)。

厄介なのは、素のCSSに // を書いてもその行が無視されるだけでは済まないことです。実際に何が起きるかを Chrome 150 で測りました。

書いたCSS登録されたルール
// sp@media 3本(上のコードのコメントだけ差し替えたもの)1本のみ@media は3本とも消滅し、幅500pxでも背景は白のまま
/* sp */@media 3本4本。@media 3本がすべて有効
// note.a.b.b のみ。直後の .a が巻き込まれて消える
.a { … } // note(行末コメント)+ .b.a のみ。次行の .b が巻き込まれて消える

理屈はこうです。CSSパーサーは // sp を「セレクタの書きかけ」として読み進め、最初に見つかった { … } をそのルールの本体だと解釈します。つまり // sp の直後にある @media ブロックが丸ごとセレクタの相方として吸い込まれ、1つの不正なルールになります。セレクタが不正なのでルールごと破棄され、コメント行1つにつき直後のルール1つが道連れになります。

元のコードは // sp// tab// pc の3行があり、それぞれが直後の @media を1本ずつ道連れにするため、3本すべてが消えます。エラーは出ず、コンソールにも何も表示されません。「メディアクエリを書いたのに何も起きない」という症状の、もっとも見つけにくい原因のひとつです。CSSが効かないときの原因切り分け全般はCSSが反映されない原因と対処法にチェックリストとしてまとめています。


境界値でやらかす3つのパターン

range構文で実際に踏みやすい失敗は3つに絞れます。いずれもエラーが出ないのが共通点です。

1つ目:<= と >= を両方書くと境界で二重に当たる

<=>= は「以下」と「以上」なので、どちらも境界値そのものを含みます。同じ値で向かい合わせると、境界のちょうど1点で両方が真になります。

/* 幅768pxのときだけ、2つとも当たる */
@media (width <= 768px) { #box { background: lightcoral; } }
@media (width >= 768px) { #box { background: lightblue; } }

幅768pxで測ると matchMedia('(width <= 768px)')matchMedia('(width >= 768px)') はどちらも true でした。実際の描画では後に書いたルールが勝ちます。上のコードでは青、順番を入れ替えると同じ幅で赤になります。詳細度が同じルールどうしなので、カスケードの後勝ちがそのまま適用されるためです。

「768pxのときだけ意図と違う」というバグはこれが原因です。片方を境界を含まない演算子にすれば排他になります。

/* 排他。767pxは赤、768pxは青 */
@media (width < 768px)  { #box { background: lightcoral; } }
@media (width >= 768px) { #box { background: lightblue; } }

2つ目:チェーン比較は方向を混ぜられない

チェーン比較で書けるのは、<<= どうし、または >>= どうしの組み合わせだけです。仕様の文法定義でも、この2形しか認められていません。

/* OK:方向が揃っている */
@media (400px < width < 1000px) { #box { background: lightgreen; } }
@media (1000px > width > 400px) { #box { background: lightgreen; } }

/* NG:方向が混ざっている。エラーは出ないが永久にマッチしない */
@media (400px < width > 1000px) { #box { background: lightcoral; } }

幅768pxで実測したところ、(400px < width < 1000px)true(400px < width > 1000px)false でした。仕様は「文法に合わないメディアクエリはパース時に not all に置き換える」と定めているため、条件は例外を投げずに、ただ静かに常時 false になります。

ここでひとつ、デバッグに直結する事実があります。文法エラーのメディアクエリは、スタイルシートから消えるわけではありません。 実測では (400px < width > 1000px)(bogus >= 0px)CSSMediaRule として登録されたままで、条件文字列もそのまま保持されていました。つまりCSSOM上には残ったまま、永久にマッチしません。⚠️ このときDevToolsのStylesパネルには何も出ません(Chrome 150で実測。Stylesパネルは条件が一致したルールだけを表示します)。// コメントの事故(ルールごと消える)と、文法エラー(ルールは残るがマッチしない)は、この点で見分けられます。

3つ目:単位を落とすと黙って無効になる

もっとも地味で、もっとも気づきにくいのがこれです。幅768pxで実測した結果を並べます。

書き方幅768pxでの結果判定
(width >= 768px)true正しい
(width>=768px)true演算子の周りの空白は省略可
( width >= 768px )true空白が多くても問題ない
(768px <= width)true値を左に置く書き方も有効
(width >= 768)falsepx の付け忘れ。0以外の数値に単位は必須
(width > = 768px)false>= の間に空白を入れてはいけない
(bogus >= 0px)false存在しないメディア特性名。例外は投げない

「演算子の前後にスペースが必要」と説明されることがありますが、実測では不要でした。逆に >= の間だけは空白を入れられません。仕様も「< または > と、続く = のあいだに空白は許されない」と明記しています。


range構文が効いているかを自分で確かめる

ここまでの落とし穴はすべて「エラーが出ない」ものでした。したがって、書いたあとに効いていることを能動的に確かめる手順を持っておく必要があります。DevToolsのConsoleに3行貼るだけで判定できます。

手順:Consoleに3行貼る

対象ページを開き、DevToolsのConsoleで次を実行します。3行目の 768 は自分が使っているブレークポイントの値に置き換えてください。

// 1) このブラウザが range 構文を解釈できるか
matchMedia('(width >= 0px)').matches

// 2) いま自分の条件がマッチしているか
matchMedia('(width >= 768px)').matches

// 3) 境界が重複していないか(true ならバグ)
matchMedia('(width < 768px)').matches && matchMedia('(width >= 768px)').matches

対応判定に CSS.supports() を使ってはいけません。実測では、range構文に対応している Chrome 150 でも CSS.supports('(width >= 0px)')false を返しました。CSS.supports() はプロパティと値の組を検査するAPIであり、メディアクエリの検査には使えないためです(MDN: CSS.supports()/2026年8月2日閲覧)。判定は matchMedia() で行ってください。

合否ライン

次の3つを満たしていれば、range構文は意図どおり評価されています。カッコ内は Chrome 150 での実測値です。

判定合格ライン実測値
① 対応確認true(width >= 0px)true
② 条件の反転境界の上下でウィンドウ幅を変えたとき truefalse が入れ替わる幅768pxで (width >= 768px)true(width > 768px)false。幅767pxでは (width >= 768px)false
③ 重複チェックfalse(同時に真にならない)排他版(< 768>= 768)は false<= 768>= 768 の組は true=バグ

外れたときの分岐

合否ラインを外したときに、次にどこを見るかを整理しておきます。ここで挙げた症状はすべて実測で再現を確認したものです。

症状意味次の一手
① が falseブラウザがrange構文に未対応か、条件文字列を打ち間違えているブラウザを更新する。更新しても false なら条件文字列を疑う。px の付け忘れ、>= のあいだの空白、特性名のタイプミスはいずれも例外を投げずに false を返す
① は true、② が幅を変えても変化しないメディアクエリ自体は有効だが、CSS側が別の理由で効いていないまず // でコメントを書いていないかを確認する。次にConsoleで [...document.styleSheets].flatMap(s => [...s.cssRules]).filter(r => r.conditionText !== undefined).map(r => r.conditionText) を実行し、条件文字列の一覧を見る。条件が配列に出れば「登録済みだが条件が偽」、出てこなければ「パース時に破棄されている」。Stylesパネルは条件が一致したルールしか表示しないので、この切り分けには使えない
③ が true境界が重複している後に書いたルールが勝つため、順番次第で結果が変わる。<>= の組み合わせに直して排他にする
② の反転位置が1pxずれるビューポート幅が小数になっているwindow.innerWidth は整数に丸められるため当てにならない(実測で幅1200.5pxのとき 1200 を返した)。判定は matchMedia() を正とする

DevToolsそのものの使い方から確認したい場合はChrome DevToolsで仮説と検証を回す手順を参照してください。


よくある質問

Q. @media (width >= 768px)@media (min-width: 768px) はどう違いますか?

同じ数値であれば意味は完全に同じです。W3C Media Queries Level 4 が「特性名に min- を付けることは >= 演算子と等価」と明記しており、実測でも幅1200.5pxで (width >= 1200px)(min-width: 1200px) は揃って true でした。違いが出るのは、旧記法に <> が無いために1pxずらして代用していたケースです。(min-width: 1201px)(width > 1200px) と等価ではなく、幅1200.5pxでは前者だけが false になります。

Q. 範囲を指定するときはカンマではなくチェーン比較を使うのですか?

連続した1つの範囲であればチェーン比較です。@media (480px <= width <= 960px) のように1本で書けます。ただし方向を混ぜることはできず、<<= どうしか >>= どうしのどちらかに揃える必要があります。実測では (400px < width > 1000px) は幅768pxでも false のままでした。離れた複数の範囲をまとめたい場合はカンマ(or)を使い、@media (width < 480px), (width > 1280px) のように書きます。

Q. and とカンマ(or)はどう使い分けますか?

条件を同時に満たす必要があるなら and、どちらか一方でよいならカンマです。例として @media (width >= 768px) and (hover: hover) は「幅768px以上、かつホバーできる環境」、@media (width < 480px), (width > 1280px) は「480px未満、または1280pxより大きい」を意味します。同じメディア特性の範囲を and でつなぐこともできますが、その用途ではチェーン比較のほうが1本で済み、境界の書き間違いも起きにくくなります。

Q. タッチ操作の端末だけホバー効果を止めたいときはどう書きますか?

打ち消しではなく付与で書きます。ベースにホバー効果を持たせず、@media (hover: hover) and (pointer: fine) { .card:hover { … } } のようにホバーできる環境にだけ足す形です。hover: none で打ち消す書き方を避けるのは、hoverpointer主たるポインティングデバイスだけを対象にするためです。W3C Media Queries Level 4 は、複数のポインティングデバイスがある場合 hover はユーザーエージェントが決めた主デバイスの特性を反映すると定めており、マウスとタッチ画面を併せ持つ端末ではどちらが主になるかで結果が変わります。利用可能なすべてのデバイスの和集合を見たい場合は any-hover / any-pointer を使います(MDN: @media / hover/2026年8月2日閲覧)。

Q. 古いブラウザ向けのフォールバックはまだ必要ですか?

原則として不要です。range構文は2025年9月27日に Baseline widely available へ到達しており、すべてのコアブラウザで使えるようになった2023年3月27日から30か月以上が経過しています。フォールバックを検討すべきなのは、業務要件でサポート対象に Safari 16.3以前 や Firefox 101以前 が明示的に含まれている場合だけです。その場合も併記で二重管理するのではなく、旧記法(min-width / max-width)に統一したほうが事故が減ります。range構文と旧記法を1つのコードベースに混在させると、どちらの境界規則で書かれた行なのかが読み取れなくなるためです。

Q. @media を書いたのに何も起きません。どこから見ればいいですか?

DevToolsのConsoleで matchMedia('(width >= 0px)').matches を実行してください。false ならブラウザの対応か条件文字列の問題、true ならCSS側の問題だと切り分けられます。CSS側だった場合は、Consoleで [...document.styleSheets].flatMap(s => [...s.cssRules]).filter(r => r.conditionText !== undefined).map(r => r.conditionText) を実行して条件文字列の一覧を見ます。条件が配列に出れば「登録されているが条件に一致していない」、出てこないなら「パース時に破棄されている」で、後者はコメント記法の事故を最初に疑ってください。Stylesパネルには条件が一致したルールしか出ないため、そこで探しても両者を区別できません。

Q. CSSに // でコメントを書いても大丈夫ですか?

大丈夫ではありません。CSSのコメントは /* */ のみで、// はSass/SCSSの記法です。素のCSSに書くと、その行が無視されるのではなく直後のルールが1つ丸ごと破棄されます。Chrome 150 での実測では、// sp のようなコメント3行を付けたスタイルシートで @media が3本とも消滅し、登録されたルールはベースの1本だけになりました。エラーも警告も出ないため、SassからプレーンCSSへ書き出す作業で最も踏みやすい事故です。


まとめ

range構文について、この記事で実測に基づいて確定させたことを整理します。

  1. range構文は2025年9月27日に Baseline widely available へ到達済み。フォールバックは原則不要
  2. 旧記法との等価は同じ数値どうしのみ。min-width: Xwidth >= Xmax-width: Xwidth <= X
  3. 1pxずらして排他にする旧来のやり方は、小数幅でどのブレークポイントも当たらない領域を作る(幅1200.5pxで再現)
  4. <=>= を向かい合わせると境界の1点で二重に当たり、後に書いたルールが勝つ
  5. チェーン比較は方向を混ぜられない。混ぜても例外は出ず、静かに常時 false になる
  6. 単位の付け忘れ、>= のあいだの空白、存在しない特性名も、すべて例外を投げずに false を返す
  7. 素のCSSに // コメントを書くと、直後のルールが丸ごと破棄される
  8. 対応検出に CSS.supports() は使えない。matchMedia() を使う

共通しているのは、間違っていてもエラーが出ないという点です。だからこそ、書いたあとに matchMedia() で3行分の確認を通す習慣が効きます。ブレークポイントを変更したときや、既存のCSSをrange構文へ移行したときは、この記事の合否ラインをそのまま当ててください。

range構文以外のメディアクエリの書き方(メディアタイプ、論理演算子、指定できるメディア特性の一覧)はメディアクエリの基本と使い方、ブレークポイントの決め方や画像の切り替えを含むレスポンシブ設計全体はレスポンシブデザイン入門にまとめています。仕様そのものを当たりたい場合は CSS Media Queries Level 4 の Range ContextMDN のメディアクエリ利用ガイドMDN: @media が出発点になります。