RORK LABJP
PLAY — Google Play's target API level 36 requirement took effect yesterday, August 31. From today, new apps and updates must target Android 16VISIBILITY — Apps still on API 35 stay listed but disappear for users on newer Android versions. No error is raised; new installs simply fade, which makes the change easy to missEXTENSION — If you missed the deadline, an extension through November 1, 2026 can be requested in Play Console — best filed alongside a concrete migration planAPPLE — On the Apple side, the event lands September 9 and iOS 27 is reported to ship September 14. Testing generated apps on iOS 27 hardware before release week is time well spentEXPO — Expo released expo-paste-input on August 28, a native module that brings image, GIF, and sticker paste to React Native TextInputEAS — EAS Observe reached general availability on August 20, putting crash and performance monitoring on the same EAS platform as builds and updatesPLAY — Google Play's target API level 36 requirement took effect yesterday, August 31. From today, new apps and updates must target Android 16VISIBILITY — Apps still on API 35 stay listed but disappear for users on newer Android versions. No error is raised; new installs simply fade, which makes the change easy to missEXTENSION — If you missed the deadline, an extension through November 1, 2026 can be requested in Play Console — best filed alongside a concrete migration planAPPLE — On the Apple side, the event lands September 9 and iOS 27 is reported to ship September 14. Testing generated apps on iOS 27 hardware before release week is time well spentEXPO — Expo released expo-paste-input on August 28, a native module that brings image, GIF, and sticker paste to React Native TextInputEAS — EAS Observe reached general availability on August 20, putting crash and performance monitoring on the same EAS platform as builds and updates
Articles/Dev Tools
Dev Tools/2026-05-20Advanced

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.

accessibilityVoiceOverDynamic Type2Rork548React Native234Reduce Motion2

Premium Article

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.

or
Unlock all articles with Membership →
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.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $15 for lifetime access
View Membership →

Related Articles

Dev Tools2026-08-22
Every bulk replace exited zero. The damage was in the lines I did not delete
Run a bulk replace over generated code and the breakage lands on the neighbouring lines, not the matched ones. Here is what broke in a live project, and a dependency-free guard that checks the invariants a replace must preserve.
Dev Tools2026-08-14
Find the native edits expo prebuild will erase before you upgrade to SDK 57
Expo SDK 57 makes expo prebuild clear and regenerate ios and android by default. Here is how to audit your hand edits first, move them into config plugins, and why 57.0.9 matters for Reanimated apps.
Dev Tools2026-08-10
Switching to Signed URLs Killed My Image Cache — Decoupling expo-image Keys from the URL
Signed URLs rewrite their query string on every expiry, so a URL-keyed cache never hits. Here is how to derive a stable key and drive expo-image's writeToCacheAsync and readFromCacheAsync yourself, with measured results.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →