JavaScript でメディアクエリ(matchMedia と MediaQueryList)

JavaScript でメディアクエリを使用してデバイスの状態(ビューポート幅や orientation など)を調べることができます。また、その状態が変化したとき (画面を回転させたり、サイズを変更したときなど) にイベントを受け取ってコールバック関数を実行することもできます。

JavaScript でメディアクエリを利用する方法(matchMedia と MediaQueryList の使い方)やビューポート幅により条件分岐する方法、一定の幅未満ではビデオを読み込まない方法について。

作成日:2023年4月26日

JavaScript でメディアクエリを使用

CSS では @media(アットルール)を使用してメディアクエリを指定し、そのメディアクエリが表す条件にマッチする場合にスタイルを適用することができます。

以下は CSS の @media を使って orientation: landscape(ビューポートの向きが横長)や orientation: portrait(ビューポートの向きが縦長)というメディアクエリを指定し、その条件にマッチした場合にスタイルを適用する例です。

CSS
@media (orientation: landscape) {
  /* ビューポートの向きが横長の場合 */
  .foo {
    color: red;
  }
}

@media (orientation: portrait) {
  /* ビューポートの向きが縦長の場合 */
  .foo {
    color: blue;
  }
}

JavaScript でも現在のドキュメントがメディアクエリの条件にマッチするかどうかを調べることができます。そのためには、メディアクエリを表す MediaQueryList オブジェクトを作成する必要があります。

MediaQueryList オブジェクトの作成

MediaQueryList オブジェクトは window.matchMedia() メソッドを使って作成します。

例えば、ビューポートの向きが横長かどうかを調たい場合は、window.matchMedia() の引数に「ビューポートの向きが横長かどうか」を表すメディアクエリ文字列 '(orientation: landscape)' を指定して MediaQueryList オブジェクトを作成します。

const mediaQueryList = window.matchMedia('(orientation: landscape)');
//ビューポートの向きが横長かどうかを表すメディアクエリ (orientation: landscape) を引数に指定

メディアクエリにマッチするかの確認

MediaQueryList オブジェクトには matches というメディアクエリにマッチするかどうかの真偽値を返すプロパティがあり、このプロパティの値を参照することで、クエリの結果(指定したメディアクエリの条件にマッチするかどうか)を確認することができます。

if (mediaQueryList.matches) {
  /* orientation は landscape(現在ビューポートは横長) */
} else {
  /* orientation は landscape ではない(現在ビューポートは縦長) */
}

状態の変化を検知(change イベント)

MediaQueryList オブジェクトの change イベントにリスナーを登録すれば、メディアクエリの状態が変化したときにコールバック関数を実行することができます。

MediaQueryList の change イベントは、メディアクエリーの状態が変化した時に発生します。

リスナーに渡されるイベント(以下では e)は、MediaQueryList オブジェクト同様、メディアクエリにマッチするかどうかの真偽値を返すプロパティ matches を持っています。

以下を記述するとビューポートの向きが変わる度にアラートを表示します。

const mediaQueryList = window.matchMedia('(orientation: landscape)');

//MediaQueryList オブジェクトの change イベントにリスナーを登録
mediaQueryList.addEventListener('change', (e) => {
  // イベント(e)の matches プロパティで判定
  if (e.matches) {
    // orientation: landscape が true
    alert('landscape');
  } else {
    // orientation: landscape が false
    alert('portrait');
  }
});

初期化処理

以下は JavaScript を使って冒頭の CSS @media を使ったメディアクエリと同じことを行う例です。

ビューポートの向き (orientation) を評価する MediaQueryList オブジェクトを作成し、一度現在のビューポートの向き(横長か縦長か)を調べて要素に反映します(初期化処理)。

この初期化処理を行わないと、ビューポートの初期状態が横長と想定しているけれど実際は縦長であるような場合に不整合が発生してしまいます。

const mediaQueryList = window.matchMedia('(orientation: landscape)');

let fooStyle = document.querySelector('.foo').style;

// 一度現在の(実際の)ビューポートの向きを調べて文字色を設定(初期化処理)
if (mediaQueryList.matches) {
  fooStyle.color = 'red';
} else {
  fooStyle.color = 'blue';
}

// ビューポートの向きが変われば文字色を変更
mediaQueryList.addEventListener('change', (e) => {
  if (e.matches) {
    fooStyle.color = 'red';
  } else {
    fooStyle.color = 'blue';
  }
});

別途リスナー関数を定義

初期化とリスナー関数の処理は同じなので、通常リスナー関数を別途定義して以下のように記述します。

addEventListener では別途リスナー関数を定義しておくことで、呼び出し時に定義された同じ関数が使われるため効率的です。

また、無名関数の場合はリスナーを削除することができませんが、別途リスナー関数を定義しておけば、必要に応じて removeEventListener() を使って削除することもできます。

リスナー関数は引数(以下では e)に change イベントのオブジェクトまたは MediaQueryList オブジェクトを受け取ります(どちらも matches プロパティを持っています)。

const mediaQueryList = window.matchMedia('(orientation: landscape)');
let fooStyle = document.querySelector('.foo').style;

// 向き変更時のリスナー関数を定義
const orientationChange = (e) => {
  if (e.matches) {
    fooStyle.color = 'red';
  } else {
    fooStyle.color = 'blue';
  }
}

// 向き変更時のリスナーを一度実行(初期化処理)
orientationChange(mediaQueryList);

// change イベントにリスナーを登録
mediaQueryList.addEventListener('change',orientationChange);
<div class="foo">ビューポートの向き(または縦横比)により文字色が変わります。</div>
ビューポートの向き(または縦横比)により文字色が変わります。

参考サイト(MDN)

window.matchMedia()

window.matchMedia() は引数にメディアクエリ文字列を指定して実行すると、現在のドキュメントがそのメディアクエリの条件にマッチするかを評価した結果を表す MediaQueryList オブジェクトを返します。

以下が window.matchMedia() の構文です。引数の mediaQueryString はメディアクエリ文字列です。

const mediaQueryListObject = window.matchMedia(mediaQueryString)

以下はビューポート幅が640px以上の場合にマッチするメディアクエリ文字列 (min-width: 640px) を引数に指定して、生成された MediaQueryList オブジェクトの matches プロパティを使って、指定したメディアクエリにマッチするかどうかを判定しています。

// 幅が640px以上のビューポートをターゲットとする MediaQueryList オブジェクトを作成
const mediaQuery = window.matchMedia('(min-width: 640px)')

// ドキュメントが指定されたメディアクエリにマッチするかを判定
if (mediaQuery.matches) {
  console.log('ビューポート幅は640px以上');
}else{
  console.log('ビューポート幅は640px未満');
}

生成した MediaQueryList オブジェクトを再利用する必要がなければ変数に代入せず、以下のように記述することができます。

 // ドキュメントが指定されたメディアクエリにマッチするかを判定
if (window.matchMedia('(min-width: 640px)').matches) {
  console.log('ビューポート幅は640px以上');
}else{
  console.log('ビューポート幅は640px未満');
}

MediaQueryList

MediaQueryList オブジェクトのプロパティとイベントを使用すると、ドキュメントが指定したメディアクエリにマッチするかどうかを判定したり、監視することができます。

MediaQueryList オブジェクトは、その matches プロパティを調べることで、ドキュメントが指定したメディアクエリの条件ににマッチしているかどうかを判定することができます。

また、MediaQueryList オブジェクトの change イベントを使うと、メディアクエリの状態が変化したときにその状態を検出することができます。

プロパティ

MediaQueryList オブジェクト(インターフェイス)には以下のプロパティがあります。

プロパティ 説明
matches 現在ドキュメントが MediaQueryList オブジェクトのメディアクエリに一致すれば true を返し、一致していなければ false を返します。
media MediaQueryList オブジェクトを作成する際に window.matchMedia() に指定したメディアクエリ文字列(シリアライズされた値)

メソッド

MediaQueryList オブジェクト(インターフェイス)には以下のメソッドがありますが、いずれも非推奨(Deprecated)となってます。

代わりに、addEventListener() や removeEventListener() を使います(次項のイベント参照)。

メソッド 説明
addListener()
※ 非推奨
メディアクエリーの状態が変化するたびに呼び出されるコールバック関数を追加します。代わりに addEventListener() で change イベントにリスナーを設定できます。
removeListener()
※ 非推奨
追加したコールバック関数を削除します。代わりに removeEventListener() を使って addEventListener() で登録したリスナーを削除することができます。

イベント

ドキュメントに対してメディアクエリを実行した結果が変更されたときに change イベントが MediaQueryList に送信されます。

例えば、メディアクエリが (min-width: 920px) の場合、 ドキュメントのビューポートの幅が 920px の閾値を通過する変更が発生する際に change イベントが発行されます。

これは、連続的に発生する resize イベントでビューポート幅を監視するより効率的です。

以下は、ビューポート幅が 920px 以上かどうかで異なる処理をするコールバック関数を定義して MediaQueryList の change イベントに登録する例です。

初期状態を正しく反映させるため、コールバック関数を一度実行しています(初期化処理)。以下の場合、この初期化処理を行わないと、初期状態でテキストも背景色も表示されません。

//メディアクエリを指定して MediaQueryList を作成
const mql = window.matchMedia('(min-width: 920px)');
const bar = document.querySelector('.bar');

// コールバック関数
const widthChange920 = (e) => {
  if (e.matches) {
    bar.textContent = 'ビューポート幅は 920px 以上です。';
    bar.style.backgroundColor = 'red';
  } else {
    bar.textContent = 'ビューポート幅は 920px 未満です。';
    bar.style.backgroundColor = 'blue';
  }
}

// コールバック関数を一度実行(初期化処理)
widthChange920(mql);

// コールバック関数をリスナーとして MediaQueryList(mql)に追加
mql.addEventListener('change', widthChange920);
<p class="bar"></p>

ビューポート幅により条件分岐

以下はドキュメントを読み込んだ時点で、ビューポート幅が 480px 以下かどうかで処理を分岐する例です。例えば、ビューポート幅が 480px 以下の場合はスマホと判定して何らかの処理が行なえます。

但し、単にビューポート幅を判定しているので、正確にスマホと判定できるわけではありません。※以下の場合、スマホを横向きの状態でページを開けば「スマホではないと判定しました。」となります。

document.addEventListener('DOMContentLoaded', () => {

  if (window.matchMedia('(max-width: 480px)').matches) {
    //ビューポート幅が480px以下の場合の処理
    console.log('スマホと判定しました。');
  } else {
    //ビューポート幅が480pxより大きい場合の処理
    console.log('スマホではないと判定しました。');
  }

});

上記の場合、ドキュメントが読み込まれた時点での処理のみ(1回だけ)になりますが、以下は change イベントを使ってその後ビューポート幅が変化して、480px の閾値を通過する変更が発生する度に何らかの処理を行う例です。

document.addEventListener('DOMContentLoaded', () => {

  // メディアクエリを指定して MediaQueryList オブジェクトを作成
  const mediaQuery = window.matchMedia('(max-width: 480px)');

  // メディアクエリの状態を評価する関数(コールバック関数)
  const checkMediaQuery = (mq) => {
    if (mq.matches) {
      //ビューポート幅が480px以下の場合の処理
      console.log('スマホと判定しました。');
    } else {
      //ビューポート幅が480pxより大きい場合の処理
      console.log('スマホではないと判定しました。');
    }
  }
  // コールバック関数に MediaQueryList オブジェクトを渡して実行(初期化処理)
  checkMediaQuery(mediaQuery);

  // 状態が変化(480pxの閾値を通過する変更が発生)したらコールバック関数を実行
  mediaQuery.addEventListener('change', checkMediaQuery);
});

一定の幅未満ではビデオを読み込まない

ビデオのファイルサイズが大きく、スマホではビデオではなく、静止画像を表示したい場合、例えば CSS で以下のようにビューポート幅が 480px 以下の場合に display:none でビデオを非表示にしても、ビデオ自体(ファイル)は読み込まれてしまいます。

/* スマホ(ビューポート幅が 480px 以下)では静止画像を表示 */
@media screen and (max-width: 480px) {
  .video-wrapper {
    background: url(images/sample.jpg);
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
  }
  .video-wrapper video {
    /* ビデオを非表示に(但し、ファイルは読み込まれてしまう) */
    display: none;
  }
}

以下は、ビューポート幅が 480px 以下の場合はスマホと判定してビデオを読み込まないようにする例です。但し、前述の例同様、ビューポート幅の判定では、正確にスマホと判定できるわけではありません。

video 要素には HTML で preload="none" を指定して、事前にファイルを読み込まないようにし、autoplay 属性は指定しません。

そして JavaScript のメディアクエリで、ビューポート幅が 480px より大きい場合は preload プロパティ(HTML の preload 属性)を変更してビデオを読み込み再生します。

<div class="video-wrapper">
  <video poster="images/fall-back-image.jpg" muted loop playsinline preload="none">
    <source src="videos/sample.webm" type="video/webm">
    <source src=".videos/sample.mp4" type="video/mp4">
  </video>
</div>

video 要素に poster 属性を指定すれば代替画像を表示できるので、CSS は以下だけでも 480px 以下の場合は画像を表示することができます。poster 属性に指定する画像とは別の画像を表示する場合は、先の CSS も記述します。

.video-wrapper {
  aspect-ratio: 16 / 9;
  overflow: hidden;
  width: 100%;
  max-width: 500px;
}
.video-wrapper video {
  width: 100%;
  height: 100%;
}

DOM の解析が完了した時点で、video 要素を取得し、ビューポート幅が 480px より大きい場合は preload プロパティに auto を設定し、autoplay プロパティに true を設定することでビデオを再生します(setAttribute を使って autoplay 属性を追加しも自動再生されません)。

「ビューポート幅が 480px より大きい場合」という条件は、「ビューポート幅 480px 以下」を表す window.matchMedia('(max-width: 480px)')! で反転させています。

document.addEventListener('DOMContentLoaded', () => {
  // video 要素を取得
  const video = document.querySelector('video');

  // ビューポート幅が480pxより大きい(480px以下でない)場合はビデオを play() で再生
  if ( !window.matchMedia('(max-width: 480px)').matches ) {
    // preload プロパティに auto を設定
    video.preload = 'auto';
    // autoplay プロパティに true を設定してビデオを再生
    video.autoplay = true;
  }
});

以下のようにメディアクエリに (min-width: 481px) を指定してもほぼ同じになります(この場合は条件を反転しない)。

document.addEventListener('DOMContentLoaded', () => {
  const video = document.querySelector('video');

  // ビューポート幅が 481px 以上の場合はビデオを再生
  if ( window.matchMedia('(min-width: 481px)').matches ) {
    // preload プロパティに auto を設定
    video.preload = 'auto';
    // autoplay プロパティに true を設定(ビデオを再生)
    video.autoplay = true;
  }
});

上記はビデオが1つの場合ですが、以下は同じページの複数のビデオを対象とする例です。

document.addEventListener('DOMContentLoaded', () => {

 //全ての video 要素を取得
 const videos = document.querySelectorAll('video');

 // (max-width: 480px) の MediaQueryList を作成
 const mql = window.matchMedia('(max-width: 480px)');

 //取得した全ての video で
 videos.forEach((video) => {
   // ビューポート幅が 480px 以下でない場合はビデオを play() で再生
   if (!mql.matches) {
    // preload プロパティに auto を設定
    video.preload = 'auto';
    // autoplay プロパティに true を設定
    video.autoplay = true;
   }
 });
});

関連ページ:video タグで動画埋め込み(背景動画)