---
title: One design system in React Native, SwiftUI, Compose and Flutter, from one token file
description: The same segmented control in React Native, SwiftUI, Jetpack Compose and Flutter: how Figma token names map, what stays the same and what follows each platform.
author: Bob Poláček
topic: Tokens
published: 2026-11-13
updated: 2026-11-13
verified: 2026-11-13
source: https://www.appetiteui.com/blog/design-system-react-native-swiftui-compose-flutter
---

# One design system in React Native, SwiftUI, Compose and Flutter, from one token file

The same segmented control in React Native, SwiftUI, Jetpack Compose and Flutter: how Figma token names map, what stays the same and what follows each platform.

By Bob Poláček, updated November 13, 2026, last verified November 13, 2026.

One design system can run in React Native, SwiftUI, Jetpack Compose and Flutter when every stack reads the same token names, takes the same component props and draws the same pixels at rest. What changes per platform is only what the operating system owns: the touch target, press feedback, haptics, text scaling and the back gesture.

This article follows one component, the segmented control, through the four stacks of Appetite UI Code. The code is the code that ships: each sample was compiled in its own toolchain before the site was allowed to print it. I build Appetite UI and Appetite UI Code, so where a number is the product's and not a platform's, the text says so.

**In short**

- One DTCG token file compiles into CSS, SwiftUI, Jetpack Compose, Flutter and React Native, so `bg/weak-50` in Figma is `--ap-bg-weak-50`, `.apBgWeak50`, `colors.bgWeak50` and `colors.apBgWeak50` in code.
- The segmented control takes the same three things in every stack: the options, the selected value and a change handler. Only the class name and the accessibility label's name differ.
- Geometry, colors, type, the selected pill and its spring are identical on iOS and Android, as in Figma.
- The hit area, press feedback, haptics and text scaling follow the platform: 44 pt and no pressed color on iOS, 48 dp and a ripple on Android.
- A rebrand is one theme file per stack, and the app stops when white labels on the brand color fall under 4.5:1.

## What does one design system look like in four codebases?

It looks like one component with four implementations that agree on names, props and pixels. The Figma Segmented Control has two properties, Size and Tabs; in code they become `size` and the number of `options`, the selected segment becomes `value` and `onValueChange`, and every stack reads the same tokens for the track, the pill and the label.

Apple describes a segmented control as "a linear set of two or more segments, each of which functions as a button". Material's counterpart is the segmented button, which Android's Compose documentation describes as a way to let people "choose from a set of options, side-by-side". The two platforms draw it differently: Material outlines each segment and marks the selected one with a check, iOS raises the selected segment inside a track. Appetite UI draws one brand control for both, a rounded track with a white pill under the chosen option, and leaves the platform the parts a person's hands expect.

**The Figma Segmented Control as props in each stack, from the Code 1.0 handoff**

| Figma | React Native | SwiftUI | Jetpack Compose | Flutter |
| --- | --- | --- | --- | --- |
| Component | `SegmentedControl` | `AppetiteSegmentedControl` | `AppetiteSegmentedControl` | `SegmentedControl` |
| Size=Default | `size="default"` | `size: .default` | `size = SegmentedControlSize.Default` | `size: SegmentedControlSize.defaultSize` |
| Tabs=3 | `options` with 3 labels | `options` with 3 labels | `options` with 3 labels | `options` with 3 labels |
| Selected segment | `value`, `onValueChange` | `value` and a trailing closure | `value`, `onValueChange` | `value`, `onValueChange` |
| Group label | `accessibilityLabel` | `accessibilityLabel` | `contentDescription` | `semanticLabel` |

From the Code 1.0 handoff for the website, the Figma variant Size=Default, Tabs=3. Read on November 13, 2026.

The label row is the one honest difference in the API. Each stack keeps its platform's own word for the text a screen reader speaks, so a SwiftUI developer writes what SwiftUI calls it and a Compose developer writes `contentDescription`.

## How do Figma token names map to each platform?

Each Figma variable keeps one name with a platform spelling: kebab case with an `--ap-` prefix in CSS, an `ap` prefix in SwiftUI and Flutter, and plain camel case in Compose and React Native. The names come from the code syntax fields in Figma, and Flutter's are derived from the CSS names because Figma has no Dart field.

The source is one file in the W3C community format, the Design Tokens Format Module, published as a Final Community Group Report on 28 October 2025. It defines a token as "information associated with a human readable name", and that name is what survives the trip. Appetite UI's file holds 589 variables in 6 collections, exported from the Figma file, and a script writes the five outputs from it.

**The tokens the segmented control reads, as each stack names them: the track, the selected pill, the chosen label, the other labels, the corner and the hit area**

| Figma variable | SwiftUI | Jetpack Compose | Flutter | React Native |
| --- | --- | --- | --- | --- |
| `bg/weak-50` | `.apBgWeak50` | `colors.bgWeak50` | `colors.apBgWeak50` | `colors.bgWeak50` |
| `bg/white-0` | `.apBgWhite0` | `colors.bgWhite0` | `colors.apBgWhite0` | `colors.bgWhite0` |
| `text/strong-950` | `.apTextStrong950` | `colors.textStrong950` | `colors.apTextStrong950` | `colors.textStrong950` |
| `text/sub-700` | `.apTextSub700` | `colors.textSub700` | `colors.apTextSub700` | `colors.textSub700` |
| `radius/full` | `Capsule()` | `RoundedCornerShape(50)` | `apRadiusFull` | `shapes.full` |
| `platform/min-touch-target` | `minTouchTarget` | `minTouchTarget` | `minTouchTarget` | `platform.minTouchTarget` |

Read in the generated token outputs and the four Segmented Control sources of Appetite UI Code 1.0 on November 13, 2026. The CSS output names the same tokens `--ap-bg-weak-50`, `--ap-radius-full` and so on.

Two rows show where the names stop being the whole story. `radius/full` is a pill: CSS and React Native store 9999 as a sentinel value, while SwiftUI draws a `Capsule()` and Compose a corner of 50 percent, which is how each platform says "fully round" without a magic number. And `platform/min-touch-target` is not a color or a size of the drawing at all. It has two values, 44 in the iOS modes and 48 in the Android mode, and the component reads it to size the area that answers a tap.

Light and Dark are one name with two values. `brand/base` is #155DFC in Light and #2B7FFF in Dark in every output, and the components never ask which mode is on. How one Figma variable becomes each of these names, modes included, is the subject of [How one Figma token becomes SwiftUI, Compose, Flutter and CSS](https://www.appetiteui.com/blog/figma-token-to-swiftui-compose-flutter-css).

## What does the same component look like in each language?

It looks like four short files that do the same thing: keep a selected range in state, pass three options and update the state on change. The samples below are a range picker with Day, Week and Month, the code the Appetite UI Code handoff lists for the website, each compiled in its own toolchain on 4 November 2026.

**RangePicker.tsx** React Native: checked with tsc in strict mode, resolved like a buyer's Expo app

```tsx
// React Native
import { useState } from 'react';
import { SegmentedControl } from '@appetite-ui/react-native';

export function RangePicker() {
  const [range, setRange] = useState('Week');
  return (
    <SegmentedControl
      options={['Day', 'Week', 'Month']}
      value={range}
      onValueChange={setRange}
      accessibilityLabel="Range"
    />
  );
}
```

**RangePicker.swift** SwiftUI: built with swift build, warnings as errors, Swift 6 language mode

```swift
// SwiftUI (AppetiteSegmentedControl.swift copied into the app target)
import SwiftUI
import AppetiteCore

struct RangePicker: View {
    @State private var range = "Week"

    var body: some View {
        AppetiteSegmentedControl(options: ["Day", "Week", "Month"], value: range, accessibilityLabel: "Range") { range = $0 }
    }
}
```

**RangePicker.kt** Jetpack Compose: compiled with Gradle, all warnings as errors

```kotlin
// Jetpack Compose
@Composable
fun RangePicker() {
    var range by remember { mutableStateOf("Week") }
    AppetiteSegmentedControl(listOf("Day", "Week", "Month"), range, { range = it }, contentDescription = "Range")
}
```

**range_picker.dart** Flutter: flutter analyze with fatal infos and warnings, no issues

```dart
// Flutter
import 'package:appetite_ui/appetite.dart';
import 'package:flutter/widgets.dart';

class RangePicker extends StatefulWidget {
  const RangePicker({super.key});

  @override
  State<RangePicker> createState() => _RangePickerState();
}

class _RangePickerState extends State<RangePicker> {
  String range = 'Week';

  @override
  Widget build(BuildContext context) {
    return SegmentedControl(
      options: const ['Day', 'Week', 'Month'],
      value: range,
      onValueChange: (v) => setState(() => range = v),
      semanticLabel: 'Range',
    );
  }
}
```

The Compose file lists no imports because the handoff gives them beside it: the runtime state functions and `com.appetiteui.components.AppetiteSegmentedControl`. Each label is its own value, so the options need unique words; Figma writes "Option" on every segment, which code can't.

## What stays the same on iOS and Android?

Everything a person sees at rest stays the same: the track, the pill, the padding, the type and the motion. The Figma component binds no platform value, so all four stacks draw the same control on both systems, and Appetite UI compares the code's renders with Figma's at 3x to hold them to it.

The numbers each source file states in its header comment are the same in all four:

- **Track.** 4 of padding on every side, `radius/full`, filled with `bg/weak-50`. At least 48 high in the Default size, bound to `spacing/12`, and 38 in Small.
- **Segments.** Equal widths, 40 high in Default and 30 in Small, with 12 of horizontal padding for two to four options and 6 for five. Apple asks for the same balance: "all segments are usually equal in width".
- **Selected pill.** `bg/white-0` with the `dropShadow/xs` effect, the label in `text/strong-950`; the others in `text/sub-700` with no fill.
- **Label.** Inter SemiBold 16 on 24 in Default, 14 on 20 in Small.
- **Motion.** The pill slides to the chosen segment on the kit's indicator spring and takes its width; a tap mid-flight retargets it. Under Reduce Motion it jumps, and the label colors still cross-fade.

Apple's advice on count fits the control too: "no more than about five segments on iPhone". The component stops at five options, the most the Figma variants draw.

Text scaling is the one place where the same rule needed one decision for all four. Each stack caps the label at the same scale, 1.353, which SwiftUI expresses as Dynamic Type up to xxxLarge, and a label that no longer fits wraps to two lines and grows the track instead of being clipped.

## What follows each platform?

The hit area, press feedback, haptics and the scale of text follow the platform, because people's hands and settings expect them. The rule in the codebase is short: a difference is allowed when the operating system offers it, when a user setting asks for it, or when every native control on that platform gives the same feedback.

That would be 'wrong' if a different behavior took place.

Flutter's documentation says this of behaviors of the operating system, such as text editing and scrolling, and separates them from conventions an app chooses, such as tabs on iOS. Appetite UI Code keeps two switches apart for it: the convention, iOS or Material, which a brand may set on purpose, and the capability of the device the app runs on, which code checks only for things that exist on one system, such as the Android ripple.

**What the segmented control does differently on iOS and Android in Appetite UI Code**

| Concern | iOS | Android |
| --- | --- | --- |
| Hit area | Grows to 44 pt, Apple's "hit region of at least 44x44 pt" | Grows to 48 dp, Google's "at least 48dpx48dp" |
| Press feedback | None, because Figma draws no pressed state for this control | A ripple at the label color and Material's pressed opacity, clipped to the segment's pill |
| Haptics | Selection feedback when the value changes | The segment tick on Android 14 and later, a context click before |
| Text scale | Dynamic Type, capped at xxxLarge | The system font scale, capped at the same 1.353 |
| Reduce Motion | The pill jumps instead of sliding | The same |

From the Segmented Control sources of Appetite UI Code 1.0 and the platform pages under Sources, read on November 13, 2026.

In React Native the Android half is two props of the platform's own `Pressable`. React Native's documentation describes `android_ripple` as the prop that "enables the Android ripple effect", and `hitSlop` as "additional distance outside of element in which a press can be detected". The library uses both: the ripple sits in a rounded view so it stays inside the pill, and the slop is computed from `platform.minTouchTarget` so a 30 point Small segment still answers a 44 or 48 square. SwiftUI, Compose and Flutter do the same with their own tools, and the visual size never changes.

The minimum sizes themselves, and where other controls fall short of them, are in [Touch target size on iOS and Android: 44 pt, 48 dp and the WCAG minimum](https://www.appetiteui.com/blog/touch-target-size-ios-android).

## How does one rebrand reach all four stacks?

Through one theme value per stack: a brand color, a radius scale and the fonts. React Native reads a `brand.ts` file, SwiftUI an `AppetiteBrand` passed to a modifier, Compose an `AppetiteBrand` passed to `AppetiteTheme`, and Flutter an `AppetiteBrand` passed to `AppetiteScope`. Every component reads its colors from the theme, so nothing else is edited.

**brand.ts** React Native: the one file a buyer edits; the other three stacks take the same values as a struct, a data class and a class

```ts
import type { AppetiteBrand } from '@appetite-ui/core';

export const brand: AppetiteBrand = {
  color: '#ca3500',
  radiusScale: 1, // radius/md is 8 at scale 1
  fonts: { body: 'Plus Jakarta Sans', heading: 'Plus Jakarta Sans' },
  fontFiles: {
    'Plus Jakarta Sans': {
      faces: {
        400: require('../../../assets/fonts/PlusJakartaSans-Regular.ttf'),
        500: require('../../../assets/fonts/PlusJakartaSans-Medium.ttf'),
        600: require('../../../assets/fonts/PlusJakartaSans-SemiBold.ttf'),
      },
      ascender: 1.038,
      descender: 0.222,
    },
  },
};
```

**OrangeBrand.swift** SwiftUI: the same brand as a struct, applied with one modifier

```swift
let orangeBrand = AppetiteBrand(
    color: "#ca3500",
    radiusScale: 1,
    bodyFont: "Plus Jakarta Sans",
    headingFont: "Plus Jakarta Sans",
    fontFiles: [
        "Plus Jakarta Sans": AppetiteBrandFontFamily(
            faces: [400: "PlusJakartaSans-Regular", 500: "PlusJakartaSans-Medium", 600: "PlusJakartaSans-SemiBold"],
            ascender: 1.038,
            descender: 0.222
        ),
    ]
)

ContentView()
    .appetiteTheme(brand: orangeBrand)
```

There is no setting called radius 8. `radiusScale` multiplies every radius except zero and full, and `radius/md`, the default control radius, is already 8 at a scale of 1. The segmented control uses `radius/full`, which no scale changes, so it stays a pill in every brand.

![Two My work screens of the Tasks example on either side of a Theme card that reads Brand color #CA3500, Radius 8, Font Plus Jakarta Sans: on the left in the default blue brand, on the right in orange, with the progress bar, the checked task and the add button orange and the Today, Upcoming and Done segmented control unchanged](https://www.appetiteui.com/images/blog/design-system-react-native-swiftui-compose-flutter--01.webp)

*One theme value changed. The segmented control keeps its neutral track and white pill; the brand reaches the progress bar, the checkbox and the add button. The Tasks example of Appetite UI Code, as drawn on the /code page.*

The color is checked before the app draws anything. Every stack computes the contrast of white labels on the derived brand colors and stops the app below 4.5:1, the WCAG AA minimum for body text. That is why the sample uses #CA3500 and not the brighter #FF6900 of an earlier mockup.

The same edit in the Figma file, and what it moves there, is in [How to rebrand a mobile app in Figma with variables](https://www.appetiteui.com/blog/rebrand-mobile-app-figma-variables).

## How do you set up one component in your stack?

Add the tokens and the core, copy in the components you use, wrap the app in the theme and set the brand. The four stacks differ in packaging, not in steps, and each folder's README gives the exact commands.

1. **Add the tokens and the core**
React Native: two packed packages, `@appetite-ui/core` and `@appetite-ui/tokens`, as file dependencies, then the Expo modules the README lists. SwiftUI: the `swiftui` folder as a local package, product `AppetiteCore`, iOS 17 or later. Compose: the `compose` folder with its `:core` and `:components` modules. Flutter: the `flutter` folder as a path dependency named `appetite_ui`.

2. **Copy in the component**
The components are source you own. For the segmented control in SwiftUI that is `AppetiteSegmentedControl.swift` and the shared helpers it lists; in React Native the library source goes into your app as `src/appetite-ui/`.

3. **Wrap the app in the theme**
`AppetiteProvider` in React Native, the `.appetiteTheme()` modifier in SwiftUI, `AppetiteTheme { }` in Compose and `AppetiteScope` in Flutter. The theme carries the scheme, the convention and the brand.

4. **Set the brand**
One color, a radius scale and fonts. Fonts other than the kit's Inter need their files in the app.

5. **Check on a device**
Turn on the largest text size, Reduce Motion and Dark, and tap the Small size near its edges.

## What ships in Appetite UI Code?

Appetite UI Code ships the components as source: every component in React Native with Expo, 41 in all, and a core set of 15 in SwiftUI, Flutter and Jetpack Compose, the segmented control among them. The rest of the native set follows in 1.x updates, free for buyers.

Beside the components it ships the token outputs for the five targets, an App Starter with three example apps (Tasks, Foodie and AI) and an agent kit generated from the code: `CLAUDE.md`, `AGENTS.md`, a Cursor rule and a skill, so an assistant writing a screen uses `AppetiteSegmentedControl` instead of inventing one. How an agent reads the Figma side is in [How Appetite UI works with AI agents: Claude, Cursor and the Core Skill](https://www.appetiteui.com/blog/appetite-ui-ai-agents-core-skill).

The segmented control above and the rest of the library, as source in your repo, from the same tokens as the Figma file.

The [Code page](https://www.appetiteui.com/code) shows the rebrand and the four stacks, the [Segmented Control docs](https://www.appetiteui.com/docs/segmented-control) list its variants in Figma, [Code output](https://www.appetiteui.com/docs/code-output) lists the token outputs and [pricing](https://www.appetiteui.com/pricing) sets out Code, the Figma file and Complete, which is both.

## Questions

### Can one design system work in React Native, SwiftUI, Compose and Flutter?

Yes, when the tokens come from one source and each component keeps the same props and the same drawing in every stack. Appetite UI Code does that from one DTCG token file, and lets the hit area, press feedback, haptics and text scale follow each platform.

### How do Figma variables become React Native tokens?

Export them to a W3C DTCG token file and generate a TypeScript module from it. In Appetite UI Code the React Native names follow Figma's Android code syntax, so `bg/weak-50` becomes `colors.bgWeak50`, and Light and Dark are two values under one name.

### Should a cross-platform component look the same on iOS and Android?

It depends on what the brand wants, and on which part. Appetite UI draws one brand component on both and adapts only what users' hands and settings expect: 44 pt or 48 dp targets, the Android ripple, haptics, text scaling and Reduce Motion.

### Why is there no radius 8 setting in the theme?

The theme takes a radius scale, a multiplier on every radius except zero and full. `radius/md`, the default control radius, is already 8 at a scale of 1, so radius 8 means the scale of 1.

### Does Appetite UI Code include every component in SwiftUI, Flutter and Compose?

Not yet. React Native has every component; SwiftUI, Flutter and Jetpack Compose start with a core set of 15, the segmented control included, and the rest follows in 1.x updates, free for buyers.

### What happens if the brand color is too light?

The app stops with an error. Each stack checks white labels on the brand color and its derived shades against 4.5:1 in Light and Dark, so #FF6900 fails at 2.89:1 in Light and #CA3500 passes at 5.22:1.

## Sources

1. [Apple Human Interface Guidelines: Segmented controls](https://developer.apple.com/design/human-interface-guidelines/segmented-controls), read November 13, 2026
2. [Apple Human Interface Guidelines: Buttons](https://developer.apple.com/design/human-interface-guidelines/buttons), read November 13, 2026
3. [Android Developers: Segmented button in Compose](https://developer.android.com/develop/ui/compose/components/segmented-button), read November 13, 2026
4. [Android Developers: Make apps more accessible](https://developer.android.com/guide/topics/ui/accessibility/apps), read November 13, 2026
5. [Flutter: Automatic platform adaptations](https://docs.flutter.dev/ui/adaptive-responsive/platform-adaptations), read November 13, 2026
6. [React Native: Pressable](https://reactnative.dev/docs/pressable), read November 13, 2026
7. [Design Tokens Community Group: Design Tokens Format Module 2025.10](https://www.designtokens.org/tr/2025.10/format/), read November 13, 2026
8. [W3C: Understanding Success Criterion 1.4.3, Contrast (Minimum)](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html), read November 13, 2026
