Flutter Cupertino Widgets: Build iOS-Style UIs (Complete Guide)

Coding Liquids blog cover featuring Sagnik Bhattacharya for the complete Flutter Cupertino widgets guide, with iOS-style navigation bars, buttons, switches, and picker visuals.
Coding Liquids blog cover featuring Sagnik Bhattacharya for the complete Flutter Cupertino widgets guide.

Cupertino widgets are Flutter's iOS-style components. When you want an app to look and feel native on an iPhone — the sliding navigation bars, the spinning-wheel pickers, the bottom action sheets — you reach for the cupertino library instead of Material. This guide covers every Cupertino widget worth knowing, with runnable code, and shows how to mix them with Material so one codebase can feel native on both platforms.

The Complete Flutter Guide course thumbnail

The Complete Flutter Guide: Build Android, iOS and Web apps

Go from scratch to building industry-standard apps with Riverpod, Firebase, animations, REST APIs, and more.

Enrol now

Import the library with import 'package:flutter/cupertino.dart';. Cupertino widgets do not need a MaterialApp — they work under a CupertinoApp — but they also work fine inside a MaterialApp, which is how most real apps mix the two. If you are new to widgets in general, read the Material widgets catalogue first; the two libraries mirror each other closely.

Follow me on Instagram@sagnikteaches

Every snippet below is paste-ready. Drop it into a CupertinoPageScaffold child, or inside a Material Scaffold body, to see it render. The examples target current stable Flutter, where the Cupertino library tracks recent iOS design closely.

Connect on LinkedInSagnik Bhattacharya

We will move from the app shell and navigation, through buttons and form controls, to the iOS-specific overlays (action sheets, pickers, and alerts) that most distinguish a native-feeling iOS app. A worked settings screen at the end ties it together.

Subscribe on YouTube@codingliquids

Material vs Cupertino: when to use which

Most teams ship a single Material design on both Android and iOS — it is less code and perfectly acceptable. Choose Cupertino when an iOS-native look is a product requirement, or use an adaptive strategy: one app that renders Material on Android and Cupertino on iOS.

  • One look everywhere → Material widgets only.
  • iOS-native look → Cupertino widgets, or branch on Platform.isIOS.
  • Native on each platform with minimal branching → use .adaptive constructors (e.g. Switch.adaptive, Slider.adaptive, CircularProgressIndicator.adaptive) which pick the right style automatically.

The app shell: CupertinoApp and CupertinoPageScaffold

CupertinoApp is the Cupertino equivalent of MaterialApp; CupertinoPageScaffold is the equivalent of Scaffold, giving you a navigation bar and a content area.

import 'package:flutter/cupertino.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return CupertinoApp(
      theme: const CupertinoThemeData(brightness: Brightness.light),
      home: CupertinoPageScaffold(
        navigationBar: const CupertinoNavigationBar(middle: Text('Home')),
        child: const Center(child: Text('Hello, Cupertino')),
      ),
    );
  }
}

Navigation: CupertinoNavigationBar and CupertinoTabScaffold

CupertinoNavigationBar is the top bar, with leading, middle, and trailing slots. For the iOS bottom tab bar, use CupertinoTabScaffold with a CupertinoTabBar.

CupertinoTabScaffold(
  tabBar: CupertinoTabBar(
    items: const [
      BottomNavigationBarItem(icon: Icon(CupertinoIcons.home), label: 'Home'),
      BottomNavigationBarItem(icon: Icon(CupertinoIcons.search), label: 'Search'),
      BottomNavigationBarItem(icon: Icon(CupertinoIcons.person), label: 'Profile'),
    ],
  ),
  tabBuilder: (context, index) => CupertinoTabView(
    builder: (context) => CupertinoPageScaffold(
      navigationBar: CupertinoNavigationBar(middle: Text('Tab $index')),
      child: Center(child: Text('Content for tab $index')),
    ),
  ),
)

Note that the icons come from CupertinoIcons, not the Material Icons set — they match the iOS SF Symbols aesthetic.

The Complete Flutter Guide course thumbnail

Ship to the App Store with confidence

The Complete Flutter Guide covers building, polishing, and publishing real iOS and Android apps end to end.

Enrol now

Buttons: CupertinoButton

CupertinoButton is the iOS button — a flat, tappable label by default, or a filled pill with CupertinoButton.filled. Pass onPressed: null to disable it.

Column(
  children: [
    CupertinoButton(
      onPressed: () {},
      child: const Text('Plain button'),
    ),
    CupertinoButton.filled(
      onPressed: () {},
      child: const Text('Filled (primary) button'),
    ),
  ],
)

Form controls: switch, slider, segmented control, text field

The iOS form controls are visually distinct from Material's and feel native on iPhone. Each is controlled — you hold the value in state and update it in the callback.

// iOS toggle
CupertinoSwitch(value: _on, onChanged: (v) => setState(() => _on = v)),

// iOS slider
CupertinoSlider(value: _v, min: 0, max: 100, onChanged: (v) => setState(() => _v = v)),

// iOS segmented control
CupertinoSegmentedControl<int>(
  groupValue: _seg,
  children: const {0: Text('Day'), 1: Text('Week'), 2: Text('Month')},
  onValueChanged: (v) => setState(() => _seg = v),
),

// iOS text field
CupertinoTextField(
  placeholder: 'Email',
  prefix: const Padding(
    padding: EdgeInsets.only(left: 8),
    child: Icon(CupertinoIcons.mail),
  ),
  onChanged: (value) {},
),

There is also CupertinoSlidingSegmentedControl, which is the newer pill-shaped segmented control with a sliding thumb — closer to current iOS.

Overlays: action sheets, dialogs, and modal popups

These overlays are what most distinguish an iOS app. CupertinoActionSheet slides up from the bottom with a list of actions; CupertinoAlertDialog is the centred confirmation dialog.

// Action sheet from the bottom
showCupertinoModalPopup<void>(
  context: context,
  builder: (context) => CupertinoActionSheet(
    title: const Text('Photo'),
    actions: [
      CupertinoActionSheetAction(onPressed: () => Navigator.pop(context), child: const Text('Take photo')),
      CupertinoActionSheetAction(onPressed: () => Navigator.pop(context), child: const Text('Choose from library')),
    ],
    cancelButton: CupertinoActionSheetAction(
      isDefaultAction: true,
      onPressed: () => Navigator.pop(context),
      child: const Text('Cancel'),
    ),
  ),
);
// Centred alert dialog returning a choice
final confirmed = await showCupertinoDialog<bool>(
  context: context,
  builder: (context) => CupertinoAlertDialog(
    title: const Text('Delete item?'),
    content: const Text('This cannot be undone.'),
    actions: [
      CupertinoDialogAction(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')),
      CupertinoDialogAction(
        isDestructiveAction: true,
        onPressed: () => Navigator.pop(context, true),
        child: const Text('Delete'),
      ),
    ],
  ),
);

Pickers: CupertinoPicker and CupertinoDatePicker

The spinning wheel is iconic to iOS. CupertinoPicker selects from a list; CupertinoDatePicker picks dates and times. Present them from the bottom with showCupertinoModalPopup.

showCupertinoModalPopup<void>(
  context: context,
  builder: (context) => Container(
    height: 250,
    color: CupertinoColors.systemBackground.resolveFrom(context),
    child: CupertinoPicker(
      itemExtent: 36,
      scrollController: FixedExtentScrollController(initialItem: _index),
      onSelectedItemChanged: (i) => setState(() => _index = i),
      children: const [Text('Small'), Text('Medium'), Text('Large')],
    ),
  ),
);
SizedBox(
  height: 220,
  child: CupertinoDatePicker(
    mode: CupertinoDatePickerMode.date,
    initialDateTime: DateTime.now(),
    onDateTimeChanged: (date) => setState(() => _date = date),
  ),
)

Loading and progress: CupertinoActivityIndicator

The iOS spinner is CupertinoActivityIndicator. Use it for indeterminate loading states in Cupertino UIs.

const CupertinoActivityIndicator(radius: 16)

Mixing Material and Cupertino: the adaptive pattern

The most maintainable way to feel native on both platforms is to keep one MaterialApp and lean on adaptive constructors, branching to Cupertino-specific widgets only where the iOS interaction genuinely differs.

// Renders a CupertinoSwitch on iOS/macOS and a Material Switch elsewhere
Switch.adaptive(value: _on, onChanged: (v) => setState(() => _on = v)),

// Branch only where the interaction differs (e.g. action sheets)
import 'dart:io' show Platform;

void showOptions(BuildContext context) {
  if (Platform.isIOS) {
    showCupertinoModalPopup(context: context, builder: _iosActionSheet);
  } else {
    showModalBottomSheet(context: context, builder: _materialSheet);
  }
}
The Complete Flutter Guide course thumbnail

Build polished iOS and Android apps

The Complete Flutter Guide takes you from widgets like these to shipping native-feeling apps on both platforms.

Enrol now

A worked example: an iOS-style settings screen

This screen uses CupertinoPageScaffold, CupertinoNavigationBar, CupertinoListSection, CupertinoListTile, CupertinoSwitch, and a destructive CupertinoAlertDialog — a realistic iOS settings page.

import 'package:flutter/cupertino.dart';

void main() => runApp(const CupertinoApp(home: SettingsScreen()));

class SettingsScreen extends StatefulWidget {
  const SettingsScreen({super.key});
  @override
  State<SettingsScreen> createState() => _SettingsScreenState();
}

class _SettingsScreenState extends State<SettingsScreen> {
  bool _notifications = true;
  bool _darkMode = false;

  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: const CupertinoNavigationBar(middle: Text('Settings')),
      child: SafeArea(
        child: ListView(
          children: [
            CupertinoListSection.insetGrouped(
              header: const Text('Preferences'),
              children: [
                CupertinoListTile(
                  title: const Text('Notifications'),
                  trailing: CupertinoSwitch(
                    value: _notifications,
                    onChanged: (v) => setState(() => _notifications = v),
                  ),
                ),
                CupertinoListTile(
                  title: const Text('Dark mode'),
                  trailing: CupertinoSwitch(
                    value: _darkMode,
                    onChanged: (v) => setState(() => _darkMode = v),
                  ),
                ),
              ],
            ),
            CupertinoListSection.insetGrouped(
              children: [
                CupertinoListTile(
                  title: const Text('Delete account',
                      style: TextStyle(color: CupertinoColors.destructiveRed)),
                  onTap: () => _confirmDelete(context),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Future<void> _confirmDelete(BuildContext context) async {
    final confirmed = await showCupertinoDialog<bool>(
      context: context,
      builder: (context) => CupertinoAlertDialog(
        title: const Text('Delete account?'),
        content: const Text('This permanently removes your data.'),
        actions: [
          CupertinoDialogAction(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')),
          CupertinoDialogAction(
            isDestructiveAction: true,
            onPressed: () => Navigator.pop(context, true),
            child: const Text('Delete'),
          ),
        ],
      ),
    );
    if (confirmed == true) {
      // perform the delete
    }
  }
}

Common Cupertino mistakes

  • Using Material Icons in a Cupertino UI. Use CupertinoIcons so icons match the iOS aesthetic.
  • Hard-coding colours. Use CupertinoColors (e.g. CupertinoColors.systemBackground.resolveFrom(context)) so colours adapt to light and dark mode.
  • Forgetting SafeArea. Cupertino layouts still need to avoid the notch and home indicator; wrap scrollable content in SafeArea.
  • Shipping Cupertino on Android. Cupertino widgets render on Android too, but an iOS look on Android feels off — branch with adaptive widgets or Platform.isIOS instead.
  • Expecting a Material SnackBar to work under CupertinoApp. Cupertino has no SnackBar; use a banner, dialog, or a custom overlay.
  • Not giving pickers a bounded height. CupertinoPicker and CupertinoDatePicker need a fixed-height container (usually presented in a modal popup).

Which Cupertino widget should I use?

  • App shell → CupertinoApp + CupertinoPageScaffold.
  • Top bar → CupertinoNavigationBar; bottom tabs → CupertinoTabScaffold + CupertinoTabBar.
  • Primary action → CupertinoButton.filled; secondary → CupertinoButton.
  • Toggle / slider → CupertinoSwitch / CupertinoSlider (or the .adaptive Material variants).
  • Choice from a list → CupertinoActionSheet or CupertinoPicker.
  • Confirm something → CupertinoAlertDialog.
  • Pick a date/time → CupertinoDatePicker.
  • Loading → CupertinoActivityIndicator.

Frequently asked questions

What are Cupertino widgets in Flutter?

They are Flutter's iOS-style components from the cupertino library, mirroring Apple's Human Interface Guidelines — CupertinoApp, CupertinoNavigationBar, CupertinoButton, CupertinoSwitch, CupertinoPicker, CupertinoAlertDialog, and more.

Should I use Material or Cupertino?

Use Material for one cross-platform or Android-first design; use Cupertino when you specifically want an iOS-native look. Many apps ship a single Material design on both platforms, or use adaptive widgets to feel native on each.

Can I mix Material and Cupertino?

Yes. Cupertino widgets work inside a MaterialApp, and adaptive constructors like Switch.adaptive render Cupertino on iOS automatically. Branch to Cupertino-specific widgets only where the iOS interaction differs.

How do I show an iOS-style picker?

Use CupertinoPicker (or CupertinoDatePicker for dates), usually presented from the bottom with showCupertinoModalPopup, and update state in the change callback.

Further reads

Keep going with the tutorials that pair with this guide:

Sources: Flutter documentation — Cupertino (iOS-style) widgets and the Widget catalog (docs.flutter.dev); Apple Human Interface Guidelines for the design conventions. Verified against current stable Flutter.