focus()メソッドの新しいオプション 前編 preventScrollオプションの活用

focus()メソッドには、preventScrollというオプションがあります。このオプションをtrueにすると、要素にフォーカスを設定した際に、現在のスクロール位置を保持します。この記事では、preventScrollオプションを使った実装と、その注意点を解説します。

発行

著者 國仲 義則 フロントエンド・エンジニア
focus()メソッドの新しいオプション シリーズの記事一覧

はじめに

UI作成を行う場合、フォーカス制御は重要な要素です。特にインタラクティブな要素を含むUIでは、適切なフォーカス制御がユーザー体験を大きく左右します。

HTMLElement.focus()メソッドは、この制御を実現するための基本的なAPIですが、単純なフォーカス設定以外にも、オプションを活用することで、より柔軟な実装が可能です。

focus()メソッドのオプションには、preventScrollfocusVisibleがあります。今回はpreventScrollオプションに焦点を当て、その使い方と注意点について解説します。

これらのオプションは新しめの機能で、まだ対応していない環境もありますが、現状でもサポート環境によっては十分活かせる場面がありますので、今のうちに覚えておきましょう。

focus()メソッドの簡単なおさらい

focus()メソッドは、要素にフォーカスを設定するためのメソッドです。基本的な使い方は次の通りです。

フォーカス可能なHTML要素に対して、focus()メソッドを呼び出すことで、その要素にフォーカスを設定します。

フォーカス可能とは、最初からそのようになっている要素(inputbuttonなど)、または、制作者がtabindex属性を設定してフォーカス可能にした任意の要素です。

簡単にコードを示します。

focus()の使用(HTML)

<p>
  <button id="trigger">次のボタンにフォーカス</button>
</p>
<p>
  <button id="target">フォーカス対象のボタン</button>
</p>

focus()の使用(CSS)

:focus {
  outline: 2px solid tomato;
}

focus()の使用(JavaScript)

const trigger = document.getElementById("trigger");
const target = document.getElementById("target");

trigger.addEventListener("click", () => {
  target.focus(); // button#targetにフォーカスを設定
});

このようなコードを書いた場合、トリガーとなるボタンをクリックすると、ターゲットのボタンにフォーカスが設定されます。

デモで確認してみましょう。

トリガーとなるボタンをクリックすると、ターゲットのボタンに枠が表示されるのが確認できます。

デモではどちらのボタンもビューポートに収まっていますが、もしターゲットがビューポート内に収まっておらず、スクロールしなければ表示できない場合、ブラウザは自動的にスクロールしてターゲットをビューポート内に収めます。

また、フォーカスが設定された要素には、ブラウザのデフォルトのフォーカスリングが表示されます。これは、ユーザーがどの要素にフォーカスがあるかを視覚的に示すためです。デモではCSSで:focus擬似クラスを使用して、フォーカスが設定された要素にスタイルを適用しています。ここでは、フォーカスリングの色をtomatoに設定しています。

このように、フォーカスリングは、CSSの:focus擬似クラスを使用してカスタマイズできます。しかし、近年は:focus-visible擬似クラスを使用して、フォーカスリングの表示を制御することが推奨されています。

:focus-visible擬似クラスにより、キーボード操作時のみフォーカスリングを表示するなど、ユーザー体験を向上させることができます。

補足:フォーカスリングの話

CodeGridでは、過去にフォーカスリングについての座談会記事があります。開発、デザインの両面からの視点で、フォーカスリングの重要性などについて語られています。

preventScrollオプションとは

focus()メソッドには、preventScrollというオプションがあります。このオプションをtrueにすると、要素にフォーカスを設定した際に、現在のスクロール位置を保持します。

preventScrollオプションは、デスクトップブラウザではサポートが充実していますが、モバイル環境のブラウザはまだ対応が不十分な場合があります。Android環境では2025年7月時点でサポートされていません。

使用方法は次のようになります。

preventScrollオプションの構文

element.focus({
  preventScroll: true // または false
});

focus()メソッドの引数にオブジェクトを渡し、その中にpreventScrollプロパティを指定します。値は真偽値です。

trueにすると、フォーカス設定時にスクロール位置が変更されません。

初期値はfalseで、スクロール位置が変更されます。falseを明示的に指定することもできますが、初期値なので省略しても問題ありません。

ユースケースに関しては後述しますので、まずは、このオプションの具体例を見てみましょう。

preventScrollの実装例(HTML)

<p>
  <a href="#sec1">ページ内リンク</a><br>
  <label><input type="checkbox" id="optionEnable" checked> preventScroll有効化</label>
</p>
<p id="sec1" tabindex="-1">
  ページ内リンクのターゲット
</p>

preventScrollの実装例(CSS)

#sec1 {
  margin-block: 100vh;
}

#sec1:focus {
  outline: 2px solid tomato;
}

preventScrollの実装例(JavaScript)

const links = document.querySelectorAll("a[href^='#']");
const checkbox = document.querySelector("#optionEnable");

links.forEach(link => {
  link.addEventListener("click", onClickLink);
});

function onClickLink(event) {
  event.preventDefault(); // 規定の動作を防ぐ
  const targetSelector = this.getAttribute("href");
  const targetElement = document.querySelector(targetSelector);

  if (targetElement) {
    targetElement.focus({
      preventScroll: checkbox.checked // スクロールを防ぐかどうか
    });
  }
}

ページの外にあるような、遠くにある要素に対してのページ内リンクが張られているコードです。preventScrollオプションの有効・無効をチェックボックスで切り替えられるようにしています。

また、ターゲットとなる要素にフォーカスが当たっていることがわかりやすいように、フォーカスリングの色にtomatoを指定しています。

このデモを動かしてみます。

preventScrollオプションを有効にすると、リンクをクリックしてもスクロール位置が変わらないことが確認できます。そのままではページ外の要素にフォーカスされたのか、わかりません。手動でスクロールしてみると、対象にフォーカスリングが表示されており、適切にフォーカスが設定されていることがわかります。

このように、focus()メソッドを使用しつつも、対象までスクロールするかどうかを決められるのがpreventScrollオプションの特徴です。前述のとおり、初期値はfalseなので、スクロールしたい場合はオプションの指定は不要です。

デモの場合ですが、フォーカスが移動したのにスクロールせずにいるとユーザーが迷子になってしまいます。デモのようにフォーカス対象がビューポート外にある可能性がある場合は、preventScrollオプションをtrueにしたら制作側がスクロールしてあげましょう。

ビューポート外の要素へのスクロール設定

const links = document.querySelectorAll("a[href^='#']");
const checkbox = document.querySelector("#optionEnable");

links.forEach(link => {
  link.addEventListener("click", onClickLink);
});

function onClickLink(event) {
  event.preventDefault(); // 規定の動作を防ぐ
  const targetSelector = this.getAttribute("href");
  const targetElement = document.querySelector(targetSelector);

  if (targetElement) {
    targetElement.focus({
      preventScroll: checkbox.checked // スクロールを防ぐかどうか
    });

    if (checkbox.checked) {
      targetElement.scrollIntoView({
        behavior: "smooth",
        block: "center" // 中央に配置
      });
    }
  }
}

フォーカス対象までスクロールするように変更しました。scrollIntoView()メソッドを使用して、スムーズにスクロールしています。

このように、preventScrollオプションを有効にしても、制作側でスクロールを行うことで、ユーザーが迷子にならないように配慮することが重要です。

補足:遷移履歴の管理

デモのようなUIを作成する場合、履歴の管理も同時に行うべきですが、focus()メソッドの説明に加えて履歴の説明があるとややこしくなるので省いています。

デモではpreventDefaultで規定の動作を防いでいるため、URLが変更されていません。それによってブラウザの進む・戻るも機能しなくなっています。

実際のアプリケーションでは、履歴管理のためにHistory APIを使用して、適切に履歴を更新することも考慮しましょう。

このオプションの使用場面としては、次のような場面などが簡単に思いつきます。

  • ビューポート外にある固定配置要素にフォーカスする場合(アニメーションでビューポート内に移動してくる、など)
  • デモのようにページ内リンクでユーザーのタイミングではなく、制作側のタイミングでスクロールしたい場合
  • ドロップダウンのセレクトメニューで最初のアイテムにフォーカスする場合

いずれにしても、フォーカスしているのにビューポート内に対象要素が見えていないとユーザーが混乱します。オプションを有効化するならば、以下の点に注意して使用しましょう。

  • 対象まで制作側がスクロールする
  • 制作側のスクロールがないなら、対象がビューポート内に存在することが前提である(または保証されている)

まとめ

今回はfocus()メソッドのオプションのひとつである、preventScrollオプションの使用と使いこなしを解説しました。スクロールを止めて、実装者がコントロールできる領域が広がります。

このオプションでスクロールを止める場合は、ユーザーが迷子にならないように必ずフォーカス対象がビューポート内に収まるようにチェックしましょう。

次回は、もうひとつのfocus()メソッドのオプションであるfocusVisibleについて解説します。