react-native-cupertino-tabs: Native iOS segmented tabs

Description:

react-native-cupertino-tabs is a React Native component that renders an iOS UISegmentedControl as a tab bar.

The package runs on the React Native New Architecture and requires react-native-nitro-modules. iOS renders the native control. Android, web, and SSR render an empty View with the requested size.

Preview

cupertino-tabs-segmented-tabs

Features

  • Native iOS rendering through a compound React API.
  • SF Symbol names plus bundled or remote image sources for tab icons.
  • Controlled selection with value or initial selection with defaultValue.
  • onValueChange returns the selected value and its index.
  • Row and column layouts through tabBarOptions.direction.
  • Track colors, selection pill color, active and inactive tints, spacing, and icon size.
  • Drag-to-select behavior and selection haptics from the native control.

Use cases

  • Music library screens can use SF Symbols and labels in a native iOS selector.
  • In settings, direction: "column" puts icons and labels into a vertically stacked tab layout.
  • A shared React Native codebase can keep its tab state in JavaScript and render this native selector behind an iOS platform check.
  • Product-specific artwork fits through custom colors and per-tab accessibility labels.

How to use it

Install the native package

The package requires the React Native New Architecture and the react-native-nitro-modules peer dependency. Use an iOS development build or a bare React Native app with the New Architecture enabled. On Android, web, and SSR, the fallback is an empty View; guard the tab bar with Platform.OS === "ios" when the native selector is required. Install CocoaPods after adding the package.

npm install react-native-cupertino-tabs react-native-nitro-modules
cd ios && pod install

Render controlled tabs

Tabs.Root uses a string value for the selected trigger. Keep the root controlled with value when the screen owns the selection state; onValueChange receives the committed value and its index.
Use defaultValue="library" for an uncontrolled root. A trigger value falls back to its label text and then its index when value is omitted. Set explicit values when labels are repeated or localized.

import { useState } from "react";
import * as Tabs from "react-native-cupertino-tabs";
export default function MediaTabs() {
  const [tab, setTab] = useState("library");
  return (
    <Tabs.Root
      value={tab}
      onValueChange={(value) => setTab(value)}
    >
      <Tabs.Trigger value="listen">
        <Tabs.Icon name="waveform" />
        <Tabs.Label>Listen</Tabs.Label>
      </Tabs.Trigger>
      <Tabs.Trigger value="library">
        <Tabs.Icon name="square.stack.fill" />
        <Tabs.Label>Library</Tabs.Label>
      </Tabs.Trigger>
      <Tabs.Trigger value="search">
        <Tabs.Icon name="magnifyingglass" />
        <Tabs.Label>Search</Tabs.Label>
      </Tabs.Trigger>
    </Tabs.Root>
  );
}

Configure the track and selection pill

Pass tabBarOptions to Tabs.Root for the native track, selected pill, layout, and feedback behavior. backgroundColor: "transparent" hides the native track while keeping the selected pill active. Colors accept hexadecimal values, rgb(), rgba(), and transparent.

<Tabs.Root
  defaultValue="home"
  tabBarOptions={{
    direction: "column",
    height: 56,
    iconSize: 22,
    backgroundColor: "transparent",
    selectedBackgroundColor: "#ffffff",
    activeTintColor: "#000000",
    inactiveTintColor: "#8e8e93",
  }}
>
  <Tabs.Trigger value="home">
    <Tabs.Icon name="house.fill" />
    <Tabs.Label>Home</Tabs.Label>
  </Tabs.Trigger>
  <Tabs.Trigger value="search">
    <Tabs.Icon name="magnifyingglass" />
    <Tabs.Label>Search</Tabs.Label>
  </Tabs.Trigger>
</Tabs.Root>

Use custom icons and labels

Tabs.Icon accepts an SF Symbol through name or an image through source. name takes priority when both are set. Source artwork keeps its own colors until the icon receives a color prop. A source icon fills its box and crops overflow like resizeMode: "cover".

<Tabs.Trigger value="profile" accessibilityLabel="Profile">
  <Tabs.Icon
    source={require("./assets/profile.png")}
    width={22}
    height={22}
    borderRadius={11}
  />
  <Tabs.Label style={{ fontSize: 13, fontWeight: "600" }}>
    Profile
  </Tabs.Label>
</Tabs.Trigger>

Component API

Compound components

Tabs.Root and Tabs.Trigger accept string identifiers. onValueChange has the signature (value: string, index: number) => void. A bare text child also works inside Tabs.Trigger when an icon or standalone label is unnecessary.

ComponentImportant propsRole
Tabs.Rootvalue, defaultValue, onValueChange, tabBarOptions, styleOwns selection and renders the native control.
Tabs.Triggervalue, disabled, accessibilityLabelDefines one selectable segment.
Tabs.Iconname, source, size, color, weight, width, height, borderRadiusRenders an SF Symbol or image icon.
Tabs.LabelstyleRenders a label with per-label typography.

tabBarOptions

These options control dimensions, colors, layout, spacing, icon size, and haptic feedback for the native control.

OptionTypeDefault
widthnumberStretches to the parent.
heightnumber44 for row, 56 for column.
backgroundColorstringSystem track.
selectedBackgroundColorstringSystem selection pill.
cornerRadiusnumberNative shape.
activeTintColorstringlabel.
inactiveTintColorstringsecondaryLabel.
direction"row" | "column""row".
gapnumber5 for row, 3 for column.
iconSizenumber17.
apportionsSegmentWidthsByContentbooleanfalse.
hapticFeedbackbooleantrue.

Interaction and platform notes

Selection commits on release. During a drag, the selection pill follows the finger immediately while icon and label colors stay with the committed tab. Releasing over another segment commits that segment.

In column layout, the control shares one icon row and one label row across segments. A trigger with no label centers its icon in the full height. Label font size does not resize the adjacent icon.

Alternatives and related resources

FAQs

Q: Does this package render a native control on Android?
A: No. The package wraps iOS UISegmentedControl. Android, web, and SSR render an empty View with the requested size.

Q: Is the React Native New Architecture required?
A: Yes. The package also requires react-native-nitro-modules as a peer dependency.

Q: When does onValueChange fire during a drag?
A: Selection commits when the finger is released. The pill follows the finger before the release, while the committed colors stay on the current tab.

Q: How do I keep a custom icon’s colors?
A: Pass the asset through source and omit the icon’s color prop. Source artwork keeps its own palette until a color is set.

Q: How do I use an uncontrolled tab root?
A: Set defaultValue on Tabs.Root and use onValueChange when the screen needs to react to a committed selection.

Add Comment