●TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13●OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude Code●SIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App Store●MAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessage●NATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core ML●SEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round●TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13●OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude Code●SIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App Store●MAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessage●NATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core ML●SEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
Production-Quality VoiceOver and Dynamic Type for Rork Apps
An indie developer's implementation notes for adding production-quality VoiceOver, Dynamic Type, and Reduce Motion support to React Native apps generated by Rork — covering label design, focus management, and regression testing.
About six months after a wallpaper app of mine had been live, a two-star review showed up: "When VoiceOver reads this app, all I hear is 'button, button, button.'" I have been shipping personal apps since 2014, and across iOS and Android the catalog has just edged past 50 million downloads — yet accessibility was sitting on my "do it properly someday" list, and had been for over six years.
I turned VoiceOver on and walked through the app myself. What looked finished visually was, in audio, nothing more than disjointed fragments. The icon buttons had no accessibilityLabel, the card rows announced their tap targets and text content as separate items, and the toggles in Settings never said whether they were on or off. VoiceOver is not a tool for some other world; it is an inspection device that bluntly tells you what your screens are failing to communicate.
Since then, whenever I start a new Rork project, I spend the first three days entirely on VoiceOver and Dynamic Type. Designing accessibility in is dramatically faster than retrofitting it, and it improves the overall implementation as a side effect. Here are the notes I keep open whenever I add accessibility to a Rork-generated React Native + Expo project — focused on label design, focus management, and regression testing, the parts the official docs cover only in fragments.
Three Ways Accessibility Pays Off for an Indie Developer
I used to assume accessibility was an ethical "should" that I could not realistically afford given my time budget. After actually doing the work, I noticed three concrete payoffs.
The first is lower store-review risk. Apple rarely rejects outright for accessibility, but Google Play's Pre-launch Report flags accessibility issues as warnings. In 2024 I had one release where contrast and missing voice labels alone added 48 hours to the review queue. For an indie release with a scheduled launch date, that delay stings emotionally as much as practically.
The second is fewer Dynamic Type complaints in store reviews. In Japan, where my wallpaper apps have a strong middle-aged user base, a non-trivial share of users runs system text at "Larger" or the Accessibility sizes. My GA4 data shows roughly 7.2 % of sessions arriving with an Accessibility content size category set. When text gets clipped for those users, low-star reviews follow immediately. In the three months after I shipped a full Dynamic Type pass, my average rating climbed from 4.2 to 4.5.
The third payoff is that VoiceOver work doubles as a structural review of your UI. Sorting out the read order surfaces logic gaps that looked fine visually — redundant regions, decorations that look tappable, opaque icons. Cleaning those up makes Dynamic Type, localization, and any future iPadOS adaptation much cheaper. Accessibility is an outward-facing principle and, at the same time, an inward-facing forced design review.
Aligning Visual Cards with Voice Cards
The most common reason VoiceOver reading breaks is that something that visually reads as a single card gets split into multiple independent elements in audio. Suppose Rork generates a product card like this:
// Before: VoiceOver makes you swipe four times per card<Pressable onPress={() => navigate('Detail', { id })}> <Image source={{ uri: thumbnail }} style={styles.thumb} /> <Text style={styles.title}>{title}</Text> <Text style={styles.price}>{`¥${price.toLocaleString()}`}</Text> <View style={styles.badge}> <Text style={styles.badgeText}>NEW</Text> </View></Pressable>
VoiceOver treats the Image, the two Text nodes, and the badge as four separate elements. The user has to swipe four times before the card is fully announced — information that the sighted user takes in at a glance gets atomized into a four-step audio walkthrough.
The fix is to roll this into one voice card:
// After: one swipe announces the whole card as a coherent unit<Pressable onPress={() => navigate('Detail', { id })} accessible={true} accessibilityRole="button" accessibilityLabel={`${title}, ${price.toLocaleString()} yen${isNew ? ', new' : ''}`} accessibilityHint="Double tap to open details"> <Image source={{ uri: thumbnail }} style={styles.thumb} accessible={false} // parent handles the announcement /> <Text style={styles.title} accessible={false}>{title}</Text> <Text style={styles.price} accessible={false}>{`¥${price.toLocaleString()}`}</Text> {isNew && ( <View style={styles.badge} accessible={false}> <Text style={styles.badgeText}>NEW</Text> </View> )}</Pressable>
Three things matter here. The parent Pressable gets accessible={true} and a combined accessibilityLabel, while the children are explicitly marked accessible={false}. The accessibilityRole="button" tells VoiceOver to announce "button" after the label so users know it is interactive. The accessibilityHint adds "what happens on double-tap" without duplicating the label — users who disable hints in VoiceOver settings simply never hear it.
I also spell out numbers in the label. Passing ¥1,200 straight through is a coin toss: depending on locale settings and the screen reader, you sometimes get "yen, one, point, two hundred." Apple often reads currency correctly, but TalkBack on Android does not always cooperate. In production I just write the value out in words within the label — ${price.toLocaleString()} yen instead of ¥${price}.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦How to align visual cards with voice cards using accessibilityElements so VoiceOver reads each card as one unit
✦Sizing math and a sensible maxFontSizeMultiplier cap that survives the full 100%–310% Dynamic Type range without breaking layout
✦Branching logic for swapping ad views and parallax when Reduce Motion or Reduce Transparency is enabled
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Using accessibilityActions for Inline Buttons on Cards
You usually want the card itself to be one voice element, but you also want to keep small secondary actions like favorite and share inside the card. Nesting Pressable components naively just leads to VoiceOver announcing the parent and the children separately, which puts the swipe count right back where it started.
On iOS, accessibilityActions lets you bundle additional operations onto a single accessible element. With the VoiceOver rotor, the user can pick "Add to favorites" or "Share" without leaving the card, so the on-screen swipe count stays low.
const cardActions = [ { name: 'favorite', label: isFav ? 'Remove from favorites' : 'Add to favorites' }, { name: 'share', label: 'Share' },];<Pressable onPress={() => navigate('Detail', { id })} accessible={true} accessibilityRole="button" accessibilityLabel={`${title}, ${price.toLocaleString()} yen`} accessibilityActions={cardActions} onAccessibilityAction={(e) => { if (e.nativeEvent.actionName === 'favorite') toggleFavorite(id); if (e.nativeEvent.actionName === 'share') shareItem(id); }}> {/* children remain accessible={false} */}</Pressable>
Android's TalkBack only partially supports accessibilityActions, so on Android I leave a visible secondary button and label it clearly — accessibilityLabel="Add to favorites, button". When platform behavior diverges, my pragmatic answer in production is Platform.OS === 'ios' branching, accepting that I am maintaining two slightly different code paths.
A Realistic Approach to Dynamic Type
Dynamic Type can scale text up to 310 % when the user enables the Accessibility sizes under Settings → Accessibility → Display & Text Size. React Native's Text follows the OS font scale by default, but a layout that hard-codes fontSize everywhere falls apart almost immediately.
My production policy has three parts. First, font sizes live in a typography preset, never as inline magic numbers. Second, allowFontScaling stays at its default true. Third, maxFontSizeMultiplier is capped at 1.6, and on screens where the UI cannot survive that cap I switch to a different layout.
I cap at 1.6 because iOS's "Accessibility max" pushes the scale to roughly 3.1 — beyond what most production layouts can absorb without three-line button labels or list cells that scroll off the screen. The trade-off is between respecting the user's setting and showing them a broken layout, and I lean toward not breaking it. VoiceOver itself is unaffected by maxFontSizeMultiplier, so users who rely on audio rather than visible text are not harmed by the cap.
When a screen genuinely needs to switch layouts at large scale factors, I read PixelRatio.getFontScale():
I landed on 1.5 after testing — that is roughly where horizontal cards stop fitting and need to flip to vertical. When working in Rork, sketching the vertical fallback up front makes the rework trivial later.
Respecting Reduce Motion and Reduce Transparency
Alongside VoiceOver, indie developers most often miss Reduce Motion and Reduce Transparency. The first matters for users with vestibular conditions or motion sensitivity, the second for users who need clearer contrast.
In React Native, AccessibilityInfo exposes both as boolean prefs:
I always plug this into ad-related animations. I used to play a flashy parallax intro before AdMob rewarded ads, but a handful of reviews mentioning "the ad screen makes me feel sick" pushed me to add a Reduce Motion branch that skips straight to the ad:
For Reduce Transparency, I swap translucent overlay cards for opaque solid cards. Ad labels and "Close" buttons placed on top of translucency tend to lose visibility fast, so designing solid as the production default is the safer baseline.
Focus Management for Modals and Screen Transitions
The most frustrating VoiceOver experience is not knowing where the focus lands after a screen change. React Navigation will read the new screen from the top by default, but modals and bottom-sheet UIs can strand focus in unexpected places.
When you need to declare "start reading here," AccessibilityInfo.setAccessibilityFocus does the job:
Returning focus to the original button after a modal closes is the part most developers forget. React Native's modal lives in a separate window-like layer, so when it dismisses, focus can jump somewhere unexpected. I hold a ref on the trigger element and restore focus in onDismiss:
That single addition wipes out the "I am lost after closing a modal" experience. It is a small detail, but VoiceOver users notice these accumulated polishes immediately.
Preventing Regressions with Detox and accessibilityIdentifier
The hardest part of accessibility is keeping it healthy as you add features. You push a new card, you forget the accessibilityLabel, and nothing complains until a user does. E2E tests are the most reliable way to catch this mechanically.
For every major screen I run a minimal Detox suite that looks like this:
Making accessibilityIdentifier (or testID in RN) mandatory on every interactive element pays off here: a regression where a label silently goes empty is caught by toHaveLabel(/.+/). Running real-device Detox in CI is expensive for an indie budget, so I run iOS Simulator Detox in a weekly GitHub Actions cron and limit it mostly to toHaveLabel-style assertions across nearly every screen.
A Pre-Submission Checklist
Before I submit a Rork-built binary, I run through this self-checklist. Every item fits inside 30 minutes.
First, turn on VoiceOver and complete one full task with one hand from the home screen to the deepest feature. If a swipe-and-double-tap flow stalls anywhere, that point is missing an accessibilityLabel or an accessibilityActions entry.
Second, set the system font size to "Accessibility max" and open every tab. Note any clipped text, unreachable buttons, or overflowing elements. Capping at 1.6 prevents most of the worst cases.
Third, enable Reduce Motion and Reduce Transparency, then try the ad screens, paywall, and onboarding — the three places where excess motion or fuzzy overlays do the most damage to user trust.
Fourth, run the Detox toHaveLabel suite in CI so any element where the label has silently emptied is surfaced before release.
Fifth, read every accessibility warning in Google Play's Pre-launch Report and App Store Connect. If there is a red flag still standing, postpone submission by one day rather than absorb a 48-hour review delay.
I have been writing code since 1997, when an early online mentor told me that "art is a natural language that is open to everyone." That phrase still echoes through how I approach accessibility — I am, in a small way, trying to make the technology I build behave like such a language. When my children grow up, even if their sight or hearing has shifted, I want the apps I made to keep working comfortably for them. That is the state I aim for whenever I ship.
Thanks for reading.
Share
Thank You for Reading
Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.