Skip to content

Accessibility

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.

An iframe sized to its content reflows and scrolls with the page, which is what zoom and keyboard users need. See Auto-resize → Accessibility.

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-origin src): directly, nothing to add inside;
  • cross-origin: the page does it itself, which any page running connectToParent does 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.

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.

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;
}