Accessibility on Mobile
By the end of this lesson you will be able to make your app work with screen readers, support users who set very large text, size touch targets for real fingers, check colour contrast, and announce important changes. These are small habits that open your app to many more people.
Accessibility means people with different abilities can use your app: people who are blind or have low vision, people with shaky hands, older users with large text turned on, or someone using the phone in bright sunlight. It is like adding a ramp next to the stairs. It helps wheelchair users, and also the person with a heavy suitcase.
How screen readers work on phones
Android has TalkBack and iOS has VoiceOver. When they are on, the user swipes left and right to move between elements, and the phone reads each one aloud: its label (what it is called), its role (what kind of thing it is) and its state (selected, disabled...). A double tap activates the focused element.
So for every important element, ask: "If I could not see the screen, what would I need to hear?" Turn on TalkBack (Settings, Accessibility) or VoiceOver (Settings, Accessibility) and try your own app. It is the fastest way to learn.
Step 1: labels and roles
Text is read automatically. But icon buttons and images have no text, so the screen reader says nothing useful, or reads a file name. Give them a label and a role.
React Native supports web-style aria-* props and role, as well as the older accessibility* props. Both work; the aria style is shorter and matches the web.
import { Pressable, Image } from "react-native";
import Ionicons from "@expo/vector-icons/Ionicons";
export function CartIconButton({ count, onPress }) {
return (
<Pressable
onPress={onPress}
role="button"
aria-label={`Cart, ${count} items`}
accessibilityHint="Opens your shopping cart"
hitSlop={12}
>
<Ionicons name="cart" size={24} />
</Pressable>
);
}
export function ShopPhoto() {
return <Image source={require("@/assets/images/icon.png")} alt="Front of Gupta Sweets shop in Rohtak" style={{ width: 120, height: 80 }} />;
}What TalkBack or VoiceOver says when the user reaches the cart icon: "Cart, 3 items, button", and after a pause, the hint "Opens your shopping cart". For the photo, it reads the alt text.
- Label: short and specific. "Cart, 3 items", not "Icon" or "Click here".
- Role:
button,link,heading,img,checkbox,radio,switch,tab,searchboxand others (the olderaccessibilityRoleprop uses slightly different names, such asheaderandimage). It tells users what will happen if they activate it. - Hint: optional extra help about the result. Do not repeat the label.
Step 2: states and grouping
When something is selected, checked, expanded or disabled, say so. Otherwise a blind user cannot tell which payment method is chosen.
import { Pressable, Text, View } from "react-native";
export function PaymentOption({ label, selected, disabled, onSelect }) {
return (
<Pressable
onPress={onSelect}
disabled={disabled}
role="radio"
aria-checked={selected}
aria-disabled={disabled}
style={{ flexDirection: "row", gap: 12, padding: 16, opacity: disabled ? 0.5 : 1 }}
>
<View style={{ width: 20, height: 20, borderRadius: 10, borderWidth: 2, backgroundColor: selected ? "#087ea4" : "transparent" }} />
<Text>{label}</Text>
</Pressable>
);
}What the screen reader says: "UPI, radio button, checked" for the chosen option, and "Cash on delivery, radio button, not checked, dimmed" (or "disabled") for an unavailable one. Because Pressable with a role is focused as one element, the circle and the text are read together, not as two separate items.
To group several texts into one stop, such as a product card with a name, price and rating, wrap them in a View with accessible={true} and a combined aria-label. To hide decorative elements from screen readers, use aria-hidden={true}.
Step 3: support large text
Many users, especially older parents and grandparents, set a large font size in phone settings. React Native scales Text automatically. Your job is to make sure layouts still work:
- Do not give text containers a fixed
height. Let them grow. - Allow long text to wrap, and use
flexShrink: 1in rows. - If a specific label truly cannot grow, such as a tab label, limit it with
maxFontSizeMultiplier={1.5}instead of turning scaling off.
import { Text, View } from "react-native";
export function PriceRow({ name, price }) {
return (
<View style={{ flexDirection: "row", justifyContent: "space-between", gap: 12, paddingVertical: 12 }}>
<Text style={{ flexShrink: 1, fontSize: 16 }}>{name}</Text>
<Text style={{ fontSize: 16, fontWeight: "600" }} maxFontSizeMultiplier={1.6}>₹{price}</Text>
</View>
);
}What you should see with the largest font setting: the dish name wraps onto two or three lines, and the price stays readable on the right instead of being pushed off the screen.
Step 4: touch targets and colour contrast
Fingers are not mouse pointers. Apple recommends touch targets of at least 44 by 44 points, and Google at least 48 by 48 density-independent pixels. Use padding or hitSlop to reach that size without making icons look huge.
Text also needs enough contrast against its background. The WCAG guideline asks for at least 4.5 to 1 for normal text and 3 to 1 for large text. You can calculate it; this is plain JavaScript, so try it:
import { createRoot } from "react-dom/client";
function luminance(hex) {
const [r, g, b] = [1, 3, 5]
.map((i) => parseInt(hex.slice(i, i + 2), 16) / 255)
.map((c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4));
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
function contrast(a, b) {
const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
return (hi + 0.05) / (lo + 0.05);
}
const pairs = [["#ffffff", "#087ea4"], ["#999999", "#ffffff"], ["#555555", "#ffffff"], ["#ffffff", "#f2b134"]];
for (const [text, bg] of pairs) {
const r = contrast(text, bg);
console.log(`${text} on ${bg}: ${r.toFixed(2)}:1 ${r >= 4.5 ? "pass" : r >= 3 ? "large text only" : "fail"}`);
}
createRoot(document.getElementById("root")).render(
<div>
{pairs.map(([text, bg]) => (
<p key={text + bg} style={{ color: text, background: bg, padding: 8 }}>
Sample text, contrast {contrast(text, bg).toFixed(2)}:1
</p>
))}
</div>
);#ffffff on #087ea4: 4.64:1 pass #999999 on #ffffff: 2.85:1 fail #555555 on #ffffff: 7.46:1 pass #ffffff on #f2b134: 1.89:1 fail
Light grey placeholder-style text on white fails, and white text on a bright yellow button is very hard to read. Dark text on the yellow would pass.
Step 5: announce changes
Sighted users notice when "Payment successful" appears. Screen reader users may not, because their focus is elsewhere. Announce important results:
import { AccessibilityInfo } from "react-native";
export async function onPaymentDone(amount) {
AccessibilityInfo.announceForAccessibility(`Payment of ₹${amount} successful`);
}
export async function shouldReduceAnimations() {
return AccessibilityInfo.isReduceMotionEnabled();
}AccessibilityInfo can also tell you whether a screen reader is on (isScreenReaderEnabled()) and whether the user prefers reduced motion, which you used in Animations with Animated & Reanimated.
Common mistakes
- Icon buttons with no label. The screen reader says "button" and nothing else.
- Labels that include the role. "Pay button" becomes "Pay button, button". Write "Pay".
- Fixed heights on text. Large font settings cut the text off.
- Turning off font scaling everywhere. Use
maxFontSizeMultiplieronly where truly needed. - Colour as the only signal. A red border alone does not tell a colour-blind user there is an error; add text.
Key points
- TalkBack and VoiceOver read each element's label, role and state.
- Use
aria-label,roleand, when helpful,accessibilityHint; give imagesalttext. - Expose states with
aria-checked,aria-selected,aria-expandedandaria-disabled. - Let text scale: no fixed text heights, wrap long text, limit growth with
maxFontSizeMultiplieronly where needed. - Touch targets should be at least 44 to 48 points; text contrast at least 4.5 to 1.
- Announce important changes with
AccessibilityInfo.announceForAccessibility.
Exercise
Make this quantity stepper accessible. It has a "-" button, a number and a "+" button. Screen reader users should hear "Decrease quantity, button" and "Increase quantity, button", the "-" button should be marked disabled at 1, and every change should announce "Quantity N". Make each button at least 44 by 44 points.
Show solution
import { useState } from "react";
import { AccessibilityInfo, Pressable, Text, View } from "react-native";
export function QuantityStepper() {
const [qty, setQty] = useState(1);
function change(next) {
setQty(next);
AccessibilityInfo.announceForAccessibility(`Quantity ${next}`);
}
const button = { width: 44, height: 44, alignItems: "center", justifyContent: "center", borderRadius: 22, backgroundColor: "#eef6fb" };
return (
<View style={{ flexDirection: "row", alignItems: "center", gap: 16 }}>
<Pressable role="button" aria-label="Decrease quantity" aria-disabled={qty === 1} disabled={qty === 1}
onPress={() => change(qty - 1)} style={button}>
<Text aria-hidden>-</Text>
</Pressable>
<Text aria-label={`Quantity ${qty}`}>{qty}</Text>
<Pressable role="button" aria-label="Increase quantity" onPress={() => change(qty + 1)} style={button}>
<Text aria-hidden>+</Text>
</Pressable>
</View>
);
}The symbols "-" and "+" are hidden from the screen reader because the buttons' labels already describe them. Each button is 44 by 44 points, and the announcement tells screen reader users the new value.
Quick Quiz
1. What three things does a screen reader usually read for an element?
Screen readers describe what an element is called, what kind of element it is and its current state.
2. An icon-only button has no label. What does the screen reader say?
Without a label, users cannot tell what the button does.
3. How should you handle users who set a very large font size?
Layouts should adapt to larger text instead of blocking it.
4. What is the minimum WCAG contrast ratio for normal text?
Normal-size text needs at least 4.5 to 1; large text needs at least 3 to 1.
Learn React Native with a mentor
Free lessons get you started. Our Mobile App Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
