Focus Guards Utility
Inject invisible focus guards at the document edges so focus transitions can be captured consistently across your app (useful for modals, overlays, and complex focus management).
Focus Guards Utility
Focus guards are invisible elements injected at the very start and end of the document that can receive focus. They enable reliable handling of focus transitions (focusin/focusout) across the entire page, which is crucial when building accessible overlays, dialogs, and other UI that manipulates focus.
Focus guards ensure focus events are catchable even at the “edges” of the DOM tree. This is particularly helpful when implementing focus-trapping and restore logic for modals and drawers.
Features
- Injects two invisible, focusable guards at the top and bottom of <body>
- Allows consistent listening for focus transitions (focusin/focusout)
- Reference-counted: guards are added once and removed when no longer needed
- Tiny footprint; safe to include at the root of an app
API
FocusGuards
A wrapper component that enables focus guards for the subtree. It injects the guards globally (under <body>) when mounted and removes them when the last user is unmounted.
Prop
Type
useFocusGuards
A hook that enables focus guards when the component mounts. Use this if you don’t want to wrap with a component and prefer enabling guards from a specific place.
Prop
Type
Both the component and the hook are safe to use multiple times. Guards are reference-counted and only added once globally; they are removed when the last reference unmounts.
Usage
1) Wrap your app (recommended)
import * as React from 'react';
import { FocusGuards } from '@loke/ui/focus-guards';
export default function App() {
return (
<FocusGuards>
{/* Your entire application */}
<RootRoutes />
</FocusGuards>
);
}2) Use the hook in a sub-tree
import * as React from 'react';
import { useFocusGuards } from '@loke/ui/focus-guards';
export function OverlayRoot({ children }: { children: React.ReactNode }) {
useFocusGuards();
return <>{children}</>;
}3) With portals or modals
Focus guards work across the whole document, so they play nicely with portals:
import * as React from 'react';
import { FocusGuards } from '@loke/ui/focus-guards';
import { createPortal } from 'react-dom';
function Modal({ open, onClose }: { open: boolean; onClose: () => void }) {
if (!open) return null;
return createPortal(
<div role="dialog" aria-modal="true" className="fixed inset-0 grid place-items-center">
<div className="rounded-md border bg-card p-6 shadow-xl">
<h2 className="mb-2 font-semibold">Dialog</h2>
<p className="text-sm text-muted-foreground">Focus-trap and restore logic can rely on guards.</p>
<button className="mt-4" onClick={onClose} type="button">Close</button>
</div>
</div>,
document.body
);
}
export function App() {
const [open, setOpen] = React.useState(false);
return (
<FocusGuards>
<button onClick={() => setOpen(true)} type="button">Open modal</button>
<Modal open={open} onClose={() => setOpen(false)} />
</FocusGuards>
);
}How it works
- Injects two <span> elements with tabIndex=0 at the beginning and end of <body>
- These spans are invisible and non-interfering (no pointer events, no layout impact)
- Ensures your application can consistently capture focusin/focusout events, even when focus moves “off the edges”
- Guard injection is reference-counted so they are only present when needed
Accessibility
- Improves reliability of keyboard focus management, especially:
- Focus trapping within dialogs and overlays
- Focus restore when closing overlays
- Compatible with standard ARIA patterns (e.g., role="dialog", aria-modal="true")
- Works alongside roving tab-index and other focus strategies
Best practices
- Enable once near your application root (<html> / <body> descendants):
- Wrap your main app in <FocusGuards>, or
- Call useFocusGuards() in a top-level layout component
- Combine with your dialog/popover focus management logic (focus-trap, initial focus, restore focus)
- Keep guards enabled for the lifetime of your interactive shell (it’s very cheap)
Troubleshooting
- “I don’t see anything in the DOM” — Guards are invisible; look for elements with data-loke-focus-guard on the document edges
- “Focus moves outside my dialog unexpectedly” — Ensure your focus-trap logic is active; guards help detect transitions, but you still need to enforce trap/restore behavior
- “Conflicts with other global focus management” — Guards are passive and should not interfere; if you see conflicts, verify other scripts aren’t removing elements with data-loke-focus-guard