Cupertino (iOS-style) Widgets
iOS-style Widgets in Flutter
Flutter includes a complete set of iOS-style widgets under the cupertino library. These widgets replicate the native look and feel of iOS apps, making your Flutter app feel at home on Apple devices. You can even mix Material and Cupertino widgets in the same app to create platform-adaptive interfaces.
package:flutter/cupertino.dart. You can import both material.dart and cupertino.dart in the same file without conflicts.CupertinoApp & CupertinoPageScaffold
CupertinoApp is the iOS equivalent of MaterialApp. It sets up Cupertino theming. CupertinoPageScaffold provides the basic page structure with a navigation bar and content area.
Basic Cupertino App
import 'package:flutter/cupertino.dart';
void main() => runApp(const MyCupertinoApp());
class MyCupertinoApp extends StatelessWidget {
const MyCupertinoApp({super.key});
@override
Widget build(BuildContext context) {
return const CupertinoApp(
title: 'iOS Style App',
theme: CupertinoThemeData(
primaryColor: CupertinoColors.activeBlue,
brightness: Brightness.light,
),
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return const CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text('Home'),
),
child: Center(
child: Text('Hello, iOS!'),
),
);
}
}
CupertinoNavigationBar
The iOS-style navigation bar sits at the top with a centred title and optional leading/trailing widgets.
Navigation Bar with Actions
CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: const Text('Settings'),
leading: CupertinoButton(
padding: EdgeInsets.zero,
onPressed: () => Navigator.pop(context),
child: const Icon(CupertinoIcons.back),
),
trailing: CupertinoButton(
padding: EdgeInsets.zero,
onPressed: () {
debugPrint('Edit tapped');
},
child: const Text('Edit'),
),
),
child: const SafeArea(
child: Center(child: Text('Content here')),
),
)
CupertinoButton
The iOS-style button with a press-down opacity effect instead of the Material ripple.
Cupertino Buttons
Column(
children: [
// Default (text) button
CupertinoButton(
onPressed: () => debugPrint('Pressed'),
child: const Text('Text Button'),
),
// Filled button
CupertinoButton.filled(
onPressed: () => debugPrint('Filled pressed'),
child: const Text('Filled Button'),
),
// Disabled button
const CupertinoButton(
onPressed: null,
child: Text('Disabled'),
),
// Custom styled button
CupertinoButton(
color: CupertinoColors.destructiveRed,
borderRadius: BorderRadius.circular(8),
onPressed: () => debugPrint('Delete'),
child: const Text('Delete Account'),
),
],
)
CupertinoTextField
The iOS-style text field with rounded borders and placeholder text.
CupertinoTextField Examples
Column(
children: [
// Basic text field
const CupertinoTextField(
placeholder: 'Enter your name',
padding: EdgeInsets.all(12),
),
const SizedBox(height: 16),
// Styled text field with prefix
CupertinoTextField(
placeholder: 'Search...',
prefix: const Padding(
padding: EdgeInsets.only(left: 8),
child: Icon(
CupertinoIcons.search,
color: CupertinoColors.systemGrey,
),
),
decoration: BoxDecoration(
color: CupertinoColors.systemGrey6,
borderRadius: BorderRadius.circular(10),
),
padding: const EdgeInsets.all(12),
clearButtonMode: OverlayVisibilityMode.editing,
),
const SizedBox(height: 16),
// Password field
const CupertinoTextField(
placeholder: 'Password',
obscureText: true,
padding: EdgeInsets.all(12),
suffix: Padding(
padding: EdgeInsets.only(right: 8),
child: Icon(
CupertinoIcons.eye_slash,
color: CupertinoColors.systemGrey,
),
),
),
],
)
CupertinoSwitch & CupertinoSlider
iOS-style toggle switches and sliders.
Switch and Slider
class ControlsExample extends StatefulWidget {
const ControlsExample({super.key});
@override
State<ControlsExample> createState() => _ControlsExampleState();
}
class _ControlsExampleState extends State<ControlsExample> {
bool _wifiEnabled = true;
double _brightness = 0.7;
@override
Widget build(BuildContext context) {
return CupertinoListSection.insetGrouped(
header: const Text('Display & Network'),
children: [
CupertinoListTile(
title: const Text('Wi-Fi'),
trailing: CupertinoSwitch(
value: _wifiEnabled,
onChanged: (bool value) {
setState(() => _wifiEnabled = value);
},
),
),
CupertinoListTile(
title: const Text('Brightness'),
trailing: SizedBox(
width: 180,
child: CupertinoSlider(
value: _brightness,
onChanged: (double value) {
setState(() => _brightness = value);
},
),
),
),
],
);
}
}
CupertinoListSection.insetGrouped to create the grouped settings-style layout common in iOS apps. It automatically handles the rounded corners and section headers.CupertinoAlertDialog
The iOS-style alert dialog with rounded corners and stacked or side-by-side action buttons.
Cupertino Alert Dialog
void _showCupertinoAlert(BuildContext context) {
showCupertinoDialog(
context: context,
builder: (BuildContext ctx) {
return CupertinoAlertDialog(
title: const Text('Delete Photo'),
content: const Text(
'This photo will be deleted from all your devices. '
'You can recover it from Recently Deleted for 30 days.',
),
actions: [
CupertinoDialogAction(
onPressed: () => Navigator.pop(ctx),
child: const Text('Cancel'),
),
CupertinoDialogAction(
isDestructiveAction: true,
onPressed: () {
Navigator.pop(ctx);
debugPrint('Photo deleted');
},
child: const Text('Delete'),
),
],
);
},
);
}
CupertinoActionSheet
An action sheet slides up from the bottom and presents a set of options. It always includes a cancel button.
Cupertino Action Sheet
void _showActionSheet(BuildContext context) {
showCupertinoModalPopup(
context: context,
builder: (BuildContext ctx) {
return CupertinoActionSheet(
title: const Text('Share Photo'),
message: const Text('Choose how you want to share this photo.'),
actions: [
CupertinoActionSheetAction(
onPressed: () {
Navigator.pop(ctx);
debugPrint('AirDrop');
},
child: const Text('AirDrop'),
),
CupertinoActionSheetAction(
onPressed: () {
Navigator.pop(ctx);
debugPrint('Messages');
},
child: const Text('Messages'),
),
CupertinoActionSheetAction(
isDestructiveAction: true,
onPressed: () {
Navigator.pop(ctx);
debugPrint('Delete');
},
child: const Text('Delete Photo'),
),
],
cancelButton: CupertinoActionSheetAction(
isDefaultAction: true,
onPressed: () => Navigator.pop(ctx),
child: const Text('Cancel'),
),
);
},
);
}
CupertinoPicker & CupertinoDatePicker
iOS-style scroll wheel pickers for selecting values and dates.
Cupertino Picker
void _showPicker(BuildContext context) {
final List<String> countries = [
'Saudi Arabia', 'UAE', 'Egypt',
'Jordan', 'Kuwait', 'Qatar',
];
showCupertinoModalPopup(
context: context,
builder: (ctx) {
return Container(
height: 250,
color: CupertinoColors.systemBackground,
child: CupertinoPicker(
itemExtent: 36,
onSelectedItemChanged: (int index) {
debugPrint('Selected: \${countries[index]}');
},
children: countries.map((c) => Center(child: Text(c))).toList(),
),
);
},
);
}
Cupertino Date Picker
void _showDatePicker(BuildContext context) {
showCupertinoModalPopup(
context: context,
builder: (ctx) {
return Container(
height: 300,
color: CupertinoColors.systemBackground,
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CupertinoButton(
child: const Text('Cancel'),
onPressed: () => Navigator.pop(ctx),
),
CupertinoButton(
child: const Text('Done'),
onPressed: () => Navigator.pop(ctx),
),
],
),
Expanded(
child: CupertinoDatePicker(
mode: CupertinoDatePickerMode.date,
initialDateTime: DateTime.now(),
minimumDate: DateTime(2000),
maximumDate: DateTime(2030),
onDateTimeChanged: (DateTime date) {
debugPrint('Date: \$date');
},
),
),
],
),
);
},
);
}
Platform-Adaptive Widgets
You can build widgets that automatically switch between Material and Cupertino styles based on the platform.
Adaptive Dialog
import 'dart:io' show Platform;
void showAdaptiveAlert(BuildContext context) {
if (Platform.isIOS || Platform.isMacOS) {
showCupertinoDialog(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: const Text('Alert'),
content: const Text('Something happened.'),
actions: [
CupertinoDialogAction(
onPressed: () => Navigator.pop(ctx),
child: const Text('OK'),
),
],
),
);
} else {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Alert'),
content: const Text('Something happened.'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('OK'),
),
],
),
);
}
}
Adaptive Switch (Built-in)
// Flutter 3.x provides Switch.adaptive and others
Switch.adaptive(
value: _isEnabled,
onChanged: (bool value) {
setState(() => _isEnabled = value);
},
)
// Also available:
// Slider.adaptive(...)
// CircularProgressIndicator.adaptive()
dart:io library is not available on Flutter Web. If your app targets web, use Theme.of(context).platform or defaultTargetPlatform from foundation.dart instead of Platform.isIOS.Practical Example: iOS Settings Clone
A realistic iOS Settings page built entirely with Cupertino widgets.
iOS Settings Screen
class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key});
@override
State<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
bool _airplaneMode = false;
bool _wifi = true;
bool _bluetooth = true;
bool _notifications = true;
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
middle: Text('Settings'),
),
child: SafeArea(
child: ListView(
children: [
CupertinoListSection.insetGrouped(
children: [
CupertinoListTile(
leading: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: CupertinoColors.systemOrange,
borderRadius: BorderRadius.circular(6),
),
child: const Icon(
CupertinoIcons.airplane,
color: CupertinoColors.white,
size: 20,
),
),
title: const Text('Airplane Mode'),
trailing: CupertinoSwitch(
value: _airplaneMode,
onChanged: (v) => setState(() => _airplaneMode = v),
),
),
CupertinoListTile(
leading: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: CupertinoColors.activeBlue,
borderRadius: BorderRadius.circular(6),
),
child: const Icon(
CupertinoIcons.wifi,
color: CupertinoColors.white,
size: 20,
),
),
title: const Text('Wi-Fi'),
additionalInfo: Text(_wifi ? 'Home Network' : 'Off'),
trailing: const CupertinoListTileChevron(),
),
CupertinoListTile(
leading: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: CupertinoColors.activeBlue,
borderRadius: BorderRadius.circular(6),
),
child: const Icon(
CupertinoIcons.bluetooth,
color: CupertinoColors.white,
size: 20,
),
),
title: const Text('Bluetooth'),
additionalInfo: Text(_bluetooth ? 'On' : 'Off'),
trailing: const CupertinoListTileChevron(),
),
],
),
CupertinoListSection.insetGrouped(
children: [
CupertinoListTile(
leading: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: CupertinoColors.systemRed,
borderRadius: BorderRadius.circular(6),
),
child: const Icon(
CupertinoIcons.bell_fill,
color: CupertinoColors.white,
size: 20,
),
),
title: const Text('Notifications'),
trailing: const CupertinoListTileChevron(),
),
],
),
],
),
),
);
}
}
Summary
CupertinoApp+CupertinoPageScaffold-- iOS app structure and page layoutCupertinoNavigationBar-- iOS-style top bar with centred titleCupertinoButton-- opacity-press button (text and filled variants)CupertinoTextField-- rounded iOS text input with placeholderCupertinoSwitch/CupertinoSlider-- iOS toggle and range controlsCupertinoAlertDialog-- rounded iOS alert with stacked actionsCupertinoActionSheet-- bottom action sheet with cancel buttonCupertinoPicker/CupertinoDatePicker-- scroll wheel selection- Platform-adaptive widgets:
Switch.adaptive, conditional platform checks
Practice Exercise
Build a complete iOS Settings screen clone using only Cupertino widgets. Include an Airplane Mode toggle, Wi-Fi and Bluetooth rows with navigation chevrons, a Notifications section, and a "Sign Out" action that shows a CupertinoActionSheet asking the user to confirm. Use CupertinoListSection.insetGrouped for the grouped layout.