SCALE — Build Lab
開発パターン · REACT HOOK

フォーカストラップ(モーダル用)

CATEGORY開発パターン TYPEReact Hook EFFORT60〜120分 DIFFICULTY
PRIMARY CODE
tsx
import { useEffect, useRef } from 'react';

const FOCUSABLE = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';

export function useFocusTrap<T extends HTMLElement>(active: boolean) {
  const ref = useRef<T>(null);
  useEffect(() => {
    if (!active || !ref.current) return;

    const previousFocus = document.activeElement as HTMLElement;
    const elements = ref.current.querySelectorAll<HTMLElement>(FOCUSABLE);
    const first = elements[0];
    const last = elements[elements.length - 1];
    first?.focus();

    const onKey = (e: KeyboardEvent) => {
      if (e.key !== 'Tab') return;
      if (e.shiftKey) {
        if (document.activeElement === first) { e.preventDefault(); last?.focus(); }
      } else {
        if (document.activeElement === last) { e.preventDefault(); first?.focus(); }
      }
    };
    document.addEventListener('keydown', onKey);
    return () => {
      document.removeEventListener('keydown', onKey);
      previousFocus?.focus();
    };
  }, [active]);
  return ref;
}
前提条件
Tailwind CSS v4TypeScript 5
USE CASES
  • 任意のダッシュボードに組み込み

フォーカストラップ(モーダル用)

:LiTarget: 用途

モーダル内に Tab フォーカスを閉じ込める。a11y 必須機能。

:LiSparkle: 特徴

  • Tab/Shift+Tab 制御
  • 最初の要素にフォーカス
  • モーダル閉じたら復元
  • a11y 準拠

:LiCode: コード(コピペ用)

import { useEffect, useRef } from 'react';

const FOCUSABLE = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';

export function useFocusTrap<T extends HTMLElement>(active: boolean) {
  const ref = useRef<T>(null);
  useEffect(() => {
    if (!active || !ref.current) return;

    const previousFocus = document.activeElement as HTMLElement;
    const elements = ref.current.querySelectorAll<HTMLElement>(FOCUSABLE);
    const first = elements[0];
    const last = elements[elements.length - 1];
    first?.focus();

    const onKey = (e: KeyboardEvent) => {
      if (e.key !== 'Tab') return;
      if (e.shiftKey) {
        if (document.activeElement === first) { e.preventDefault(); last?.focus(); }
      } else {
        if (document.activeElement === last) { e.preventDefault(); first?.focus(); }
      }
    };
    document.addEventListener('keydown', onKey);
    return () => {
      document.removeEventListener('keydown', onKey);
      previousFocus?.focus();
    };
  }, [active]);
  return ref;
}

:LiHandPointer: 使い方

対象プロジェクトに該当ファイルをコピーして、props を流し込むだけ。

:LiAlertCircle: 注意事項

  • 依存パッケージを忘れず追加