Focus Order & Keyboard Navigation
Hardware keyboard and switch control on tablets. A cookbook for focus order, external keyboards, and switch navigation on iPad, Android tablets, and foldables - where touch-first layouts break for non-touch users.
Search across all documentation pages
Hardware keyboard and switch control on tablets. A cookbook for focus order, external keyboards, and switch navigation on iPad, Android tablets, and foldables - where touch-first layouts break for non-touch users.
Quick-reference recipe card - copy-paste ready.
import { useRef } from "react";
import { FlatList, Pressable, Text, TextInput, View, StyleSheet } from "react-native";
export function KeyboardFriendlyForm() {
const emailRef = useRef<TextInput>(null);
const passwordRef = useRef<TextInput>(null);
return (
<View style={styles.form}>
<Text nativeID="emailLabel" style={styles.label}>
Email
</Text>
<TextInput
ref={emailRef}
accessibilityLabel="Email"
accessibilityLabelledBy="emailLabel"
returnKeyType="next"
blurOnSubmit={false}
onSubmitEditing={() => passwordRef.current?.focus()}
style={styles.input}
/>
<Text nativeID="passwordLabel" style={styles.label}>
Password
</Text>
<TextInput
ref={passwordRef}
accessibilityLabel="Password"
accessibilityLabelledBy="passwordLabel"
secureTextEntry
returnKeyType="done"
style={styles.input}
/>
<Pressable
accessibilityRole="button"
accessibilityLabel="Sign in"
focusable
style={styles.button}
>
<Text style={styles.buttonText}>Sign in</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
form: { padding: 16, gap: 8 },
label: { fontWeight: "600" },
input: { borderWidth: 1, borderColor: "#cbd5e1", borderRadius: 8, padding: 12 },
button: { minHeight: 44, backgroundColor: "#2563eb", borderRadius: 8, justifyContent: "center", alignItems: "center" },
buttonText: { color: "#fff", fontWeight: "600" },
});When to reach for this: Tablet layouts, hardware keyboard accessories, Switch Control users, and any screen where assistive tech must traverse controls in a predictable sequence - especially master-detail and modal flows.
Master-detail on a tablet: selecting a list item moves screen reader focus and keyboard focus into the detail header.
import { useEffect, useRef, useState } from "react";
import {
AccessibilityInfo,
FlatList,
Pressable,
Text,
View,
StyleSheet,
findNodeHandle,
} from "react-native";
type Item = { id: string; title: string; body: string };
const DATA: Item[] = [
{ id: "1", title: "Invoice #1042", body: "Due April 12" },
{ id: "2", title: "Invoice #1043", body: "Paid" },
];
export function MasterDetail({ width }: { width: number }) {
const isSplit = width >= 768;
const [selectedId, setSelectedId] = useState(DATA[0].id);
const detailTitleRef = useRef<Text>(null);
const selected = DATA.find((d) => d.id === selectedId)!;
useEffect(() => {
if (!isSplit) return;
const node = findNodeHandle(detailTitleRef.current);
if (node) {
AccessibilityInfo.setAccessibilityFocus(node);
}
}, [selectedId, isSplit]);
return (
<View style={styles.split}>
<FlatList
style={styles.list}
data={DATA}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<Pressable
accessibilityRole="button"
accessibilityLabel={item.title}
accessibilityState={{ selected: item.id === selectedId }}
focusable
onPress={() => setSelectedId(item.id)}
style={[styles.row, item.id === selectedId && styles.rowSelected]}
>
<Text>{item.title}</Text>
</Pressable>
)}
/>
{isSplit ? (
<View style={styles.detail} accessibilityLabel="Invoice details">
<Text
ref={detailTitleRef}
accessibilityRole="header"
style={styles.detailTitle}
>
{selected.title}
</Text>
<Text>{selected.body}</Text>
</View>
) : null}
</View>
);
}
const styles = StyleSheet.create({
split: { flex: 1, flexDirection: "row" },
list: { flex: 1, maxWidth: 320 },
row: { padding: 16, minHeight: 48 },
rowSelected: { backgroundColor: "#e0f2fe" },
detail: { flex: 2, padding: 24, gap: 8 },
detailTitle: { fontSize: 22, fontWeight: "700" },
});What this demonstrates:
focusable on list rows for hardware keyboard traversalaccessibilityState.selected exposes list selection to TalkBackAccessibilityInfo.setAccessibilityFocus moves VoiceOver to the detail header on selection changeReact Native does not support CSS order-style focus reordering. Structure JSX so the native tree matches the intended sequence.
// Preferred - title before actions in JSX
<View>
<Text accessibilityRole="header">Account</Text>
<Pressable accessibilityRole="button" accessibilityLabel="Edit profile" />
<Pressable accessibilityRole="button" accessibilityLabel="Sign out" />
</View>
// Avoid - actions visually positioned above title with absolute layout
// Screen readers still read in JSX order unless you hide/restructure| Technique | When to use |
|---|---|
| Reorder JSX | Default - cheapest and most reliable |
importantForAccessibility="no-hide-descendants" | Skip decorative columns |
setAccessibilityFocus | After navigation or selection changes |
Merge with accessible on parent | Single stop for complex tappable rows |
| Key / action | Expected behavior | RN support |
|---|---|---|
| Tab / Shift+Tab | Move between focusable controls | Android + iPad external keyboard (platform-dependent) |
| Enter / Space | Activate focused button | Pressable with focusable |
| Arrow keys in lists | Often maps to scroll, not item focus | Supplement with explicit list semantics |
| Escape | Close modal | Wire BackHandler + focus restore |
<Pressable
focusable
accessibilityRole="button"
accessibilityLabel="Close"
onPress={onClose}
/>TextInput is focusable by default - chain fields with returnKeyType="next" and refsSwitch Control (iOS) and Switch Access (Android) scan focusable elements. Reduce stops:
<Pressable
accessibilityRole="button"
accessibilityLabel={`${productName}, ${price}, add to cart`}
onPress={addToCart}
style={styles.row}
>
<View importantForAccessibility="no-hide-descendants" accessibilityElementsHidden>
{/* visual layout */}
</View>
</Pressable>accessibilityActions on adjustable controls - see accessibilityLabel & accessibilityRoleimport { Modal, Pressable, Text, View, StyleSheet } from "react-native";
export function ConfirmModal({ visible, onConfirm, onCancel }: {
visible: boolean;
onConfirm: () => void;
onCancel: () => void;
}) {
return (
<Modal visible={visible} animationType="fade" transparent>
<View style={styles.backdrop}>
<View
style={styles.sheet}
accessibilityViewIsModal
accessibilityLabel="Confirm delete"
>
<Text accessibilityRole="header">Delete item?</Text>
<Pressable
focusable
accessibilityRole="button"
accessibilityLabel="Confirm delete"
onPress={onConfirm}
>
<Text>Delete</Text>
</Pressable>
<Pressable
focusable
accessibilityRole="button"
accessibilityLabel="Cancel"
onPress={onCancel}
>
<Text>Cancel</Text>
</Pressable>
</View>
</View>
</Modal>
);
}
const styles = StyleSheet.create({
backdrop: { flex: 1, backgroundColor: "rgba(0,0,0,0.5)", justifyContent: "center", padding: 24 },
sheet: { backgroundColor: "#fff", borderRadius: 12, padding: 20, gap: 12 },
});accessibilityViewIsModal prevents VoiceOver from reaching content under the sheetsetAccessibilityFocus back to the triggering controlFor deterministic hardware focus jumps on Android TV or custom keyboards:
<Pressable
nativeID="filterButton"
focusable
nextFocusDown="resultsList"
/>nativeID on the target viewCross-link layout patterns:
// Wide: list + detail side by side - focus moves to detail on select
// Narrow: stack push to detail route - focus lands on detail header on mountSee Split View & Tablet Layouts for routing structure; this page covers focus behavior after layout splits.
tabIndex - use focusable and native hierarchy.setAccessibilityFocus.| Setup | What to verify |
|---|---|
| VoiceOver + iPad keyboard | Tab through form; Enter activates buttons |
| TalkBack + Bluetooth keyboard | Focus order on master-detail |
| Switch Control (iOS) | Scan hits each actionable row once |
| TalkBack reading controls | Headings navigable via rotor-equivalent |
Pressable.display: 'none' traps focus; unmount inactive routes or hide descendants.TextInput and focusable pressables are the core building blocks.accessibilityActions and focus management.AccessibilityInfo.setAccessibilityFocus(reactTag) with findNodeHandle(ref.current).Text or View with a clear accessibilityRole="header".Stack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 16, 2026