●MAX — Rork Max generates native Swift apps for iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessage●NATIVE — Rork Max unlocks native features: AR/LiDAR, Metal 3D, widgets, Dynamic Island, Live Activities, HealthKit, and Core ML●STACK — Rork builds native iOS and Android apps with React Native (Expo) from a plain-English description●GROWTH — Rork now attracts over 743,000 monthly visits, growing at an 85% rate●PRICE — Rork is free to start, with paid plans from $25/month●TREND — Gartner projects 75% of new apps will be built with low-code/no-code by the end of 2026●MAX — Rork Max generates native Swift apps for iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessage●NATIVE — Rork Max unlocks native features: AR/LiDAR, Metal 3D, widgets, Dynamic Island, Live Activities, HealthKit, and Core ML●STACK — Rork builds native iOS and Android apps with React Native (Expo) from a plain-English description●GROWTH — Rork now attracts over 743,000 monthly visits, growing at an 85% rate●PRICE — Rork is free to start, with paid plans from $25/month●TREND — Gartner projects 75% of new apps will be built with low-code/no-code by the end of 2026
Building a Segmented Control With a Sliding Indicator in Reanimated
The stock segmented control looks out of place on Android. Here is a custom Reanimated component that measures each segment and slides the indicator, with complete code plus UI-thread rendering, accessibility, and RTL handling from real shipping notes.
I rebuilt the "Light / Dark / Auto" switch on a wallpaper app's settings screen not long ago. At first I dropped in @react-native-segmented-control as-is, but on Android the corners and colors looked foreign, and the switch just snapped from one state to the next. After lifting my finger, there was no easy way for my eyes to follow where the selection had landed. It felt oddly restless.
When the selected position glides across to its new spot, that single beat of motion makes the screen feel settled in the hand. In this article I want to walk through the custom "sliding indicator" I built with Reanimated, from measuring widths to the last details before shipping.
Why I didn't stop at the stock component
SegmentedControl leans native on iOS, but on Android it's hard to keep visually consistent, and there's almost no room for animation. I wanted the same expression on both platforms, and I wanted to tune the indicator's motion myself. Those two wishes are what pushed me toward building my own.
Here's a rough comparison to frame the decision.
Concern
Stock SegmentedControl
Custom Reanimated
Unified look across OSes
Hard
Free
Indicator motion
Mostly fixed
Tunable
Implementation cost
Low
Medium
Accessibility
Left to the OS
Your responsibility
If the stock control is enough, the stock control is best. I only lean custom where motion becomes part of the experience, like a settings screen or filter UI that should match a brand's color and movement.
The core idea: measure widths, then move
This is where people trip. Segment labels change width by language, whether Japanese or English. If you assume equal columns and compute position from screen width / count, the indicator drifts off the labels in languages with longer words.
So I measure each segment's real size with onLayout and use that array to drive the indicator's translateX and width. Measuring happens once. Every switch afterward just reads the measured values, so no re-render occurs.
✦
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
✦Complete code that measures segment widths via onLayout and slides the indicator
✦A design that runs on the UI thread with no re-renders (feels like 60fps)
✦The finishing touches that ship: accessibilityRole and RTL support
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.
Why animate both width and translateX? Because label widths differ, the indicator has to stretch too, or its edges spill past the label. Putting both on the same withSpring makes moving and stretching one unified motion, and nothing breaks. The pointerEvents="none" on the indicator prevents the white pill from swallowing taps meant for the segments underneath.
Tuning the motion: spring or timing
I first built it with a 200ms withTiming, but it felt mechanical. Switching to withSpring at damping: 18 / stiffness: 180 is where I settled. Here's a rough guide.
Goal
Recommendation
Crisp, businesslike
withTiming 160–200ms
Soft, settles in the hand
withSpring damping 18 / stiffness 180
Kill the bounce
Raise damping (around 22)
The numbers decide "character," not "speed." A calm spring for settings, a slight bounce for a game-like UI. Deciding by hand, while touching it, turned out to be the most reliable way.
Three things that tripped me up
First, on the initial render the width is still 0, so the indicator's width is 0 and it vanishes for a frame. The harm is small, but if it bothers you, disable animation for the first frame only and enable it once widths are measured.
Second, RTL (Arabic and others) flips the order. React Native mirrors flexDirection: "row" automatically, but translateX is computed by hand, so the sign is off. Read I18nManager.isRTL and invert the offset direction.
Third, accessibility. A visual tab needs to reach a screen reader as a "tab" too. Omit accessibilityRole="tab" and accessibilityState={{ selected }}, and VoiceOver reads it only as a "button," never conveying the selected state. I always verify this on a real device before shipping.
The change you feel
This part resists numbers, but as far as I measured on real devices (iPhone 13 / Pixel 6), the indicator lives entirely in a worklet on the UI thread, so I observed no JS-thread frame drops even when hammering the switch. The only re-render is the label color on value change. Because the motion itself runs on shared values, it doesn't stutter even when you switch mid-scroll in a list.
It's a small part, but after you lift your finger, the selection quietly follows to its place. That single beat lifts the overall impression of care across the whole app. If you'd like to take it further with gesture-driven dragging, please also see advanced animation with Reanimated and gesture-handler.
I hope it helps with your implementation. Thank you 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.