Accessibility
A name for every iframe
Section titled “A name for every iframe”Screen readers announce an iframe by its title. Give every one a title that describes
its content, e.g. title="Order summary". In development, <Frame> warns about a missing
or generic title, one that another <Frame> already has, and a negative tabIndex.
For a cross-origin page, the page itself can provide its title:
useIframeTitle.
No scroll box of its own
Section titled “No scroll box of its own”An iframe sized to its content reflows and scrolls with the page, which is what zoom and keyboard users need. See Auto-resize → Accessibility.
Disabling an iframe
Section titled “Disabling an iframe”To make an iframe’s content unusable for a while, behind a modal or in a read-only
preview, use useIframeInert:
import { useIframeInert } from 'react-iframe-kit';
const [iframe, setIframe] = useState<HTMLIFrameElement | null>(null);useIframeInert(iframe, modalOpen);
return <iframe ref={setIframe} title="Editor" src={url} />;While it’s on, nothing inside can be clicked, focused or typed into. The inert
attribute on the <iframe> alone isn’t enough: in Chromium and Safari, keys still reach
a field that had focus inside, or one the page focuses itself. So the hook also makes the
page inside inert:
- same-origin (a
<Frame>, or a same-originsrc): directly, nothing to add inside; - cross-origin: the page does it itself, which any page running
connectToParentdoes automatically. A page without it only gets the attribute, which is enough in Firefox but not in Chromium and Safari.
The hook owns the iframe’s inert attribute while it’s on, so don’t also pass an
inert prop.
Iframes in focus-trapped dialogs
Section titled “Iframes in focus-trapped dialogs”A modal keeps keyboard focus inside itself with a focus-trap library. Depending on the library and the browser, the content of an iframe in the dialog can end up unreachable by keyboard: Tab skips it, or wraps around before getting there. It happens when the iframe is the first or the last focusable thing in the dialog.
What we measured, with a cross-origin iframe as the first or the last element of a dialog:
| Library | As is | tabIndex={0} on the iframe |
Focus guards (below) |
|---|---|---|---|
focus-trap (and focus-trap-react) |
❌ every browser | ✅ Chromium, WebKit · ❌ Firefox | ✅ |
| react-focus-lock (Chakra UI) | ✅ | ✅ | ✅, with two extra Tab stops |
Radix FocusScope (Radix Dialog) |
✅ Chromium, WebKit · ❌ Firefox | ✅ Chromium, WebKit · ❌ Firefox | ✅ |
❌ means one or both Tab directions never enter the iframe. Tested with focus-trap 8.2.2, react-focus-lock 2.13.7 and @radix-ui/react-focus-scope 1.1.16, in Playwright’s Chromium, Firefox and WebKit (Playwright 1.63). Other libraries weren’t tested: check yours by tabbing through the dialog in each browser.
- focus-trap decides what is tabbable with a selector that doesn’t match
<iframe>, so it never counts the iframe as a stop.tabIndex={0}makes it count, which is enough in Chromium and WebKit. - Firefox, with focus-trap or Radix, doesn’t move focus into the iframe when the library wraps around to it. Only focus guards help there.
Focus guards
Section titled “Focus guards”Put a focusable element on each side of the iframe, inside the dialog. The library then wraps to the guard instead of the iframe, and the browser’s own Tab moves between the guard and the iframe’s content. Give them text, so a screen reader says where the user is, and show them while focused, so sighted keyboard users don’t lose focus:
function FocusGuard({ children }: { children: string }) { return ( <span tabIndex={0} className="focus-guard"> {children} </span> );}
<Dialog> <FocusGuard>Start of the embedded form</FocusGuard> <iframe title="Payment" src="https://pay.example.com/form" /> <FocusGuard>End of the embedded form</FocusGuard></Dialog>;.focus-guard:not(:focus) { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap;}