FloatingFocusManager: rAF-deferred guard redirect loses a fast/synthetic Tab race (focus escapes modal) — request sync redirect or preventDefault option

Author: williamjingoCreated Aug 20, 2026Updated Aug 20, 2026

Environment: @floating-ui/[email protected], Chromium 149.0.7827.55 via Playwright 1.61.1, macOS 26.6.2, Intel i5-1038NG7 (4c/8t). Reproduced with and without CDP Emulation.setCPUThrottlingRate 4x/6x. (Found via @material-tailwind/[email protected]'s Dialog, which renders <FloatingFocusManager context={context}> with all defaults, but the mechanism is library-level.)

Summary: with modal + default guards, the focus-guard sentinels redirect focus via enqueueFocus(), which schedules the actual .focus() on a requestAnimationFrame rather than synchronously, and the guard never calls preventDefault() on the Tab keydown. The browser's native Tab-traversal default therefore races the rAF-deferred redirect — when Tab events arrive faster than a paint frame, document.activeElement observably lands on the page behind the modal (real interactive elements, e.g. a login form's submit button) for one or more presses. A real, if narrow, focus-containment escape — silent, no error.

Minimal reproduction (library components only):

javascript
import { useFloating, FloatingFocusManager, FloatingPortal, useDismiss, useInteractions } from "@floating-ui/react";

function Modal({ open, onOpenChange }) {
  const { refs, context } = useFloating({ open, onOpenChange });
  const { getFloatingProps } = useInteractions([useDismiss(context)]);
  if (!open) return null;
  return (
    <FloatingPortal>
      <FloatingFocusManager context={context} modal>
        <div ref={refs.setFloating} {...getFloatingProps()} role="dialog">
          <button>Not now</button>
          <button>Confirm</button>
        </div>
      </FloatingFocusManager>
    </FloatingPortal>
  );
}
// Background page: several tabbable elements (nav links) outside the modal.
// Drive with a synthetic keydown loop (no preventDefault), bisecting delayMs:
for (let i = 0; i < 18; i++) {
  document.activeElement.dispatchEvent(new KeyboardEvent("keydown", { key: "Tab", bubbles: true }));
  await sleep(delayMs);
}

Measured thresholds (2 runs/cell): deterministic escape at 0/4/8ms inter-press delay; deterministic trap at 12ms+. Under 4x/6x CPU throttling the escape did NOT extend to higher delays — it collapsed (4x: 8ms → trapped 2/2) or became non-deterministic (6x: 0ms and 8ms → 1/2) — suggesting the window is tied to rAF/event-loop scheduling internals rather than wall-clock delay.

Why it matters: consumers relying on FloatingFocusManager for WCAG focus-containment have no guarantee against event sources faster than a paint frame. Ordinary human cadence (~100ms+) and even the fastest OS key-repeat settings (~20-40ms) sit above the measured window on this hardware, but the relationship to fast assistive-technology event rates is untested — we'd value the maintainers' read on whether the window can widen on slower hardware/browsers.

The ask: either (a) a sync/immediate option so the guard redirect focuses synchronously in the same event-handler tick, or (b) preventDefault() on the guard-adjacent Tab keydown with a manually computed focus target, removing the reliance on the native default action racing the rAF callback.

Full bisection transcripts (normal + throttled) available on request.