ShipNative
PricingBlogFAQContact
Build my app
ShipNative
PricingBlogFAQContact
Build my app
GuideOctober 2026 · 7 min read · By Saurabh Singh

React Native Segmented Control: Native vs Custom (2026)

A segmented control is the pill-shaped toggle that switches a screen between List and Map, or a chart between Week, Month, and Year. iOS has had it forever as UISegmentedControl. Android has nothing with the same name and Material 3 calls the nearest thing segmented buttons. React Native removed its own wrapper years ago. So the question is really two questions: do you want the native iOS control with an imitation on Android, or one custom control that looks identical everywhere? This guide gives you both, with working code, and the props and accessibility details that get skipped.

The options

OptioniOSAndroidPick it forThe cost
@react-native-segmented-control/segmented-controlReal UISegmentedControlJS imitation of the iOS styleiOS-first apps that want the exact system controlTwo different looks; limited styling on iOS
Custom (Pressable + Reanimated)Your designYour designIdentical on both platforms, matches your theme60 lines you maintain; accessibility is on you
react-native-paper SegmentedButtonsMaterial 3 outlined buttonsMaterial 3 outlined buttonsApps already on Paper and Material 3Looks Android-flavoured on iOS
react-native-ui-lib SegmentedControlThemed pill controlThemed pill controlApps already using ui-libPulls in a large design system

The honest default in 2026 is the custom control. The native package is excellent on iOS and merely fine on Android, and most apps today have a design system that neither platform’s stock control matches anyway. Use the native package when you are deliberately building an iOS-native-feeling app and Android is secondary.

Option 1: the native package

npx expo install @react-native-segmented-control/segmented-control
import { useState } from 'react';
import SegmentedControl from '@react-native-segmented-control/segmented-control';

const RANGES = ['Week', 'Month', 'Year'] as const;

export function RangePicker({ onChange }: { onChange: (r: (typeof RANGES)[number]) => void }) {
  const [index, setIndex] = useState(0);

  return (
    <SegmentedControl
      values={[...RANGES]}
      selectedIndex={index}
      onChange={(e) => {
        const i = e.nativeEvent.selectedSegmentIndex;
        setIndex(i);
        onChange(RANGES[i]);
      }}
      // iOS 13+ style on both platforms; omit for the older bordered iOS look
      appearance="dark"           // 'light' | 'dark'; defaults to the system
      tintColor="#f97316"         // the selected pill
      fontStyle={{ color: '#aaa' }}
      activeFontStyle={{ color: '#fff', fontWeight: '600' }}
      style={{ height: 36 }}
    />
  );
}

Three props do the styling: tintColor for the selected pill, backgroundColor for the track, and the two fontStyle props for label colour and weight. That is roughly all UIKit allows, which is the point of a native control: it looks like the OS and you do not fight it. Set appearance explicitly if your app forces a theme, otherwise it follows the system and can mismatch a dark-only screen.

Two things to know. The onChange event wraps the index in nativeEvent.selectedSegmentIndex, with a convenience onValueChange that gives you the label string. And on Android and web the component is a JavaScript render of the iOS look, so the same props work but the result sits in a Material app like a visitor. If that bothers you, read on.

Option 2: a custom control with a sliding indicator

The whole thing is a row of Pressables over an absolutely-positioned pill whose left edge is animated. Measure the container once, divide by the number of segments, and move the pill with a spring:

import { useState } from 'react';
import { LayoutChangeEvent, Pressable, StyleSheet, Text, View } from 'react-native';
import Animated, { useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
import * as Haptics from 'expo-haptics';

type Props<T extends string> = {
  options: readonly T[];
  value: T;
  onChange: (v: T) => void;
};

const PAD = 3;

export function SegmentedControl<T extends string>({ options, value, onChange }: Props<T>) {
  const [width, setWidth] = useState(0);
  const segment = width > 0 ? (width - PAD * 2) / options.length : 0;
  const index = Math.max(0, options.indexOf(value));
  const x = useSharedValue(0);

  // Animate the pill whenever the selected index or the measured width changes.
  x.value = withSpring(index * segment, { damping: 18, stiffness: 220, mass: 0.6 });

  const pillStyle = useAnimatedStyle(() => ({ transform: [{ translateX: x.value }] }));

  const onLayout = (e: LayoutChangeEvent) => setWidth(e.nativeEvent.layout.width);

  return (
    <View style={styles.track} onLayout={onLayout} accessibilityRole="tablist">
      {segment > 0 && (
        <Animated.View style={[styles.pill, { width: segment }, pillStyle]} pointerEvents="none" />
      )}
      {options.map((opt) => {
        const selected = opt === value;
        return (
          <Pressable
            key={opt}
            style={styles.segment}
            accessibilityRole="tab"
            accessibilityState={{ selected }}
            accessibilityLabel={opt}
            onPress={() => {
              if (selected) return;
              Haptics.selectionAsync();
              onChange(opt);
            }}
          >
            <Text style={[styles.label, selected && styles.labelSelected]} numberOfLines={1}>
              {opt}
            </Text>
          </Pressable>
        );
      })}
    </View>
  );
}

const styles = StyleSheet.create({
  track: {
    flexDirection: 'row',
    height: 40,
    padding: PAD,
    borderRadius: 12,
    backgroundColor: 'rgba(120,120,128,0.18)',
  },
  pill: {
    position: 'absolute',
    top: PAD,
    bottom: PAD,
    left: PAD,
    borderRadius: 9,
    backgroundColor: '#ffffff',
    // Android needs elevation, iOS needs shadow*, set both.
    elevation: 2,
    shadowColor: '#000',
    shadowOpacity: 0.12,
    shadowRadius: 4,
    shadowOffset: { width: 0, height: 2 },
  },
  segment: { flex: 1, alignItems: 'center', justifyContent: 'center' },
  label: { fontSize: 14, fontWeight: '500', color: 'rgba(60,60,67,0.7)' },
  labelSelected: { color: '#000', fontWeight: '600' },
});

Usage is a controlled component like any other:

const [view, setView] = useState<'List' | 'Map'>('List');

<SegmentedControl options={['List', 'Map'] as const} value={view} onChange={setView} />
{view === 'List' ? <ResultsList /> : <ResultsMap />}

The details that make it feel right: the pill is measured from the container rather than hard-coded, so it works at any width and with any number of segments. The spring is slightly underdamped so the pill settles with a hint of overshoot, which is what the iOS control does. And Haptics.selectionAsync() gives the tiny click that UIKit plays for free. Drop that line if you are not on Expo, or guard it with a platform check; it is a no-op on Android devices without a haptic engine.

Option 3: Paper SegmentedButtons for Material apps

If the app is built on react-native-paper, the Material 3 version is already installed:

import { SegmentedButtons } from 'react-native-paper';

<SegmentedButtons
  value={view}
  onValueChange={setView}
  buttons={[
    { value: 'list', label: 'List', icon: 'format-list-bulleted' },
    { value: 'map', label: 'Map', icon: 'map' },
  ]}
/>

It supports icons, a multiSelect mode, and the Paper theme. It also looks unmistakably Material, outlined buttons joined at the edges rather than a pill in a track, which is right on Android and slightly foreign on iOS. That is the trade: platform-correct on one side, or identical on both.

Four things that get skipped

  • Accessibility roles. Three bare Pressables announce as three unrelated buttons. The tablist and tab roles with accessibilityState.selectedmake VoiceOver say “Month, tab, 2 of 3, selected.” The native package does this for you; the custom one above does it explicitly.
  • Long labels. Segments are equal width, so one long label truncates while its neighbours have room. Keep labels to one or two words, or let the control scroll horizontally, at which point it has become a chip row.
  • Do not drive navigation with it. If each segment is a route with its own back stack, that is a tab bar or a top tab navigator. A segmented control that pushes screens confuses the back button on Android. The tab bar guide covers that case.
  • Keep the content swap cheap. Switching List to Map should not unmount and rebuild a map view every time. Keep both mounted and toggle visibility, or at minimum memoise the heavy child, so the control feels instant rather than tied to a loading state.

Skip the wiring

A measured track, a spring-animated pill, proper tab roles, a haptic tick. Sixty lines, written once per app and slightly differently each time. ShipNative generates a real Expo app from a description with components like this already in the design system, previews it on your phone, and gives you the full React Native project to keep. Describe the screen with its List and Map toggle, then edit the control like any file you wrote.

Frequently Asked Questions

Does React Native have a segmented control component?

Not in core anymore. SegmentedControlIOS was removed from React Native in 0.59 and lives on as the community package @react-native-segmented-control/segmented-control, which renders a real UISegmentedControl on iOS and a JavaScript imitation on Android and web. It works in Expo Go. Many teams instead build a custom segmented control so both platforms look identical.

How do I use a segmented control in Expo?

Run npx expo install @react-native-segmented-control/segmented-control, then render SegmentedControl with a values array, a selectedIndex, and an onChange handler that reads event.nativeEvent.selectedSegmentIndex. No config plugin or dev build is needed; the native iOS view is part of the package and the Android side is plain JavaScript.

What does @react-native-segmented-control look like on Android?

A JavaScript recreation of the iOS 13 style: a rounded grey track with a white sliding pill. It is serviceable but it is not a Material component, so it can look out of place next to Material buttons and chips. If your Android design is Material 3, use react-native-paper SegmentedButtons or a custom control instead.

Segmented control or tabs: which should I use?

A segmented control switches the view or filter of one piece of content in place, like List vs Map or Week vs Month, and lives inside the screen. Tabs switch between separate top-level sections and live in the navigation structure. If each option has its own route and back-stack, use a tab bar; if it filters what is already on screen, use a segmented control.

How do I make a custom segmented control accessible?

Give the container accessibilityRole="tablist" or "radiogroup", give each segment accessibilityRole="tab" (or "radio") with accessibilityState={{ selected }}, and keep each touch target at least 44 points tall. Screen readers then announce "Week, tab, 1 of 3, selected" instead of three unrelated buttons.

→

React Native Tab Bar: Custom and Floating

When the options are whole sections, not a filter, you want this instead.

Read guide →
→

React Native Animations with Reanimated

The shared-value basics behind the sliding indicator.

Read guide →

Ship a real React Native app today

Describe, preview, and export Expo code, free to start.

Build with ShipNative
ShipNative

Build mobile apps with AI. Describe it, try it on your phone, own the code.

Features

Text to App AIApp Generator from ScreenshotPRD to Mobile App

Tools

All free toolsApp Cost CalculatorApp Name GeneratorApp Store Keyword ToolReact Native Components

Blog

All blog postsHow to Build an App Without CodingBest AI Tools for Real Mobile AppsExpo EAS App Store ChecklistLovable, Cursor & v0 for MobileBest AI App Builders in 2026React Native AI App Builder Guide
© 2026 ShipNative
PrivacyTermsAboutFAQPricingDiscordShipNative - Featured on Startup FameFeatured on Twelve ToolsFeatured on TinyShelfShipNative - Launched on Fazier