Accessibility
Name the dialog, manage focus, expose clear controls, and test every dismissal path.
Magic Modal provides the dialog container around the component passed to show(). The top stack
entry receives modal semantics. Modal content still owns its heading, instructions, fields, button
labels, validation messages, and loading announcements.
Pass a localized accessibilityLabel with each modal:
const result = await magicModal.show<ConfirmationResult>(ConfirmationModal, {
accessibilityLabel: "Confirm account deletion",
swipeDirection: undefined,
});The label names the dialog for assistive technology. Keep a matching visible heading inside the component.
Modal content
Give every action an accessible role and label. Include a visible close or cancel control so the interaction never depends on a backdrop press or swipe.
import { Pressable, Text, View } from "react-native";
import { useMagicModal } from "magic-modal";
type ConfirmationResult = {
confirmed: boolean;
};
export function ConfirmationModal() {
const { hide } = useMagicModal<ConfirmationResult>();
return (
<View>
<Text accessibilityRole="header">Delete this account?</Text>
<Text>This removes the account from every signed-in device.</Text>
<Pressable
accessibilityLabel="Keep account"
accessibilityRole="button"
onPress={() => hide({ confirmed: false })}
>
<Text>Cancel</Text>
</Pressable>
<Pressable
accessibilityLabel="Delete account permanently"
accessibilityRole="button"
onPress={() => hide({ confirmed: true })}
>
<Text>Delete account</Text>
</Pressable>
</View>
);
}Use accessibilityLiveRegion for status text that changes while the modal stays open. Associate
form labels and errors with their fields using the accessibility properties supported by the
target.
Web focus and keyboard behavior
The web dialog moves focus to an element with autoFocus, the first interactive element, or the
dialog container. Tab and Shift+Tab remain inside the top modal. Closing restores focus to the
element that had it before show().
Escape follows the system-dismiss path. The default handler closes the modal and resolves its
promise with BACK_BUTTON_PRESS. An onBackButtonPress override can guard the close:
magicModal.show(UnsavedFormModal, {
accessibilityLabel: "Edit billing address",
onBackButtonPress: ({ hide }) => {
if (canDiscardChanges()) {
hide({ reason: MagicModalHideReason.BACK_BUTTON_PRESS });
}
},
});The portal gives the wrapper role="dialog" and aria-modal="true" on web. Custom HTML content
should provide normal semantic headings, form labels, and buttons inside that wrapper. The backdrop
stays outside the accessibility tree.
Native behavior
The dialog wrapper exposes the dialog role on native. iOS also receives accessibilityViewIsModal
on the portal and active dialog. The accessibility escape action follows the same system-dismiss
handler as web Escape. Android's system back action already uses that handler.
Only the top stack entry remains interactive and visible to accessibility services. When the top entry closes, the previous modal becomes active again.
Background isolation outside the portal remains app-owned on Android, since those siblings live outside the portal tree. Verify the complete screen with TalkBack in the application's real root layout.
Dismissal results
Handle every close path in the caller:
const result = await magicModal.show<FormResult>(FormModal, {
accessibilityLabel: "Create profile",
});
switch (result.reason) {
case MagicModalHideReason.INTENTIONAL_HIDE:
return saveProfile(result.data);
case MagicModalHideReason.BACKDROP_PRESS:
case MagicModalHideReason.SWIPE_COMPLETE:
case MagicModalHideReason.BACK_BUTTON_PRESS:
case MagicModalHideReason.GLOBAL_HIDE_ALL:
return saveDraft();
}Test before shipping
- Open the modal from a keyboard and confirm focus moves inside.
- Press Tab and Shift+Tab through every control.
- Press Escape, close through the visible button, tap the backdrop, and use the Android back action.
- Confirm focus returns to the trigger after the web modal closes.
- Open two stacked modals and confirm assistive technology reaches the top entry.
- Check the accessible name, reading order, labels, errors, and status announcements with VoiceOver and TalkBack.
- Test text scaling, orientation changes, and a small screen with the largest supported font size.