◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Expo SDK 58 is out of beta as of 10/15 (beta since 09/15). Check your dependencies before upgrading●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Expo SDK 58 is out of beta as of 10/15 (beta since 09/15). Check your dependencies before upgrading●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
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 TypeRork585React Native239Reduce Motion

✦ 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 $10 for lifetime access
View Membership →

Related Articles

⬡ Dev Tools2026-09-04
EAS secret visibility does not keep a value out of your app — deciding prefix and visibility separately
The EXPO_PUBLIC_ prefix decides what ships inside your app; EAS visibility decides who can read it. Why stacking them blanks a value on OTA updates, and how to check your build.
⬡ 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.