원클릭으로
sheet-modals
Bottom sheets, modals, action sheets, and title sheets with search and navigation
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Bottom sheets, modals, action sheets, and title sheets with search and navigation
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Specialized widgets and themes for Music, Stocks, Weather, App Store, and Books apps
Button widgets in iOS Design System - CupertinoButtonWidget and ButtonWidget with all variants
Guide to creating new widgets following iOS Design System patterns and conventions
Icons, tags, titles, descriptions, dividers, and markdown rendering for iOS Design System
Using flutter_hooks for cleaner state management in iOS Design System widgets
Text fields, switches, pickers, date pickers, and form validation for iOS Design System
| name | sheet-modals |
| description | Bottom sheets, modals, action sheets, and title sheets with search and navigation |
iOS-style bottom sheets and modal presentations with blur effects, drag handling, and search capabilities.
Versatile bottom sheet with draggable swipe-to-dismiss.
await CupertinoSheetWidget.showModalSheet(
context: context,
enableDrag: true, // Default true
useNestedNavigation: false,
title: TitleSheetWidget.standard(
title: 'Select Option',
leading: CloseButtonWidget(
onPressed: () => Navigator.pop(context),
),
separator: const DividerWidget(),
),
children: (context) => [
RowWidget.standard(
title: 'Option 1',
description: null,
leftWidget: IconWidget.background(
iconData: CupertinoIcons.star,
),
rightWidget: null,
displayDivider: true,
onPressed: () {
Navigator.pop(context, 'option1');
},
onLongPress: null,
decorationCallback: null,
),
RowWidget.standard(
title: 'Option 2',
description: null,
leftWidget: null,
rightWidget: null,
displayDivider: false,
onPressed: () {
Navigator.pop(context, 'option2');
},
onLongPress: null,
decorationCallback: null,
),
],
)
await CupertinoSheetWidget.showCupertinoModalSheet(
context: context,
filter: kImageFilterBlur, // Blur background
barrierColor: const SystemMaterialsBackgroundsColorsDark().thick,
barrierDismissible: true,
enableDrag: true,
title: TitleSheetWidget.standard(
title: 'Details',
leading: CloseButtonWidget(
onPressed: () => Navigator.pop(context),
),
),
children: (context) => [
Padding(
padding: const EdgeInsets.all(16),
child: Text('Modal content here'),
),
],
)
await CupertinoSheetWidget.showStandardModalSheet(
context: context,
title: 'Choose a category',
leading: CloseButtonWidget(
onPressed: () => Navigator.pop(context),
),
separator: const DividerWidget(),
children: (context) => [
RowWidget.standard(
title: 'Sports',
description: null,
leftWidget: IconWidget.background(
iconData: CupertinoIcons.sportscourt,
),
rightWidget: null,
displayDivider: true,
onPressed: () => Navigator.pop(context, 'sports'),
onLongPress: null,
decorationCallback: null,
),
],
)
For multi-step flows within sheets:
await CupertinoSheetWidget.showModalSheet(
context: context,
useNestedNavigation: true, // Enable nested navigation
title: TitleSheetWidget.standard(
title: 'Settings',
leading: null,
),
children: (context) => [
RowWidget.standard(
title: 'Account',
description: null,
leftWidget: null,
rightWidget: const Icon(CupertinoIcons.chevron_right),
displayDivider: true,
onPressed: () {
Navigator.of(context).push(
CupertinoPageRoute(
builder: (context) => AccountSettingsScreen(),
),
);
},
onLongPress: null,
decorationCallback: null,
),
],
)
// Apple Pay Modal Sheet 01
await CupertinoSheetWidget.showApplePayModalSheet01(
context: context,
title: 'Confirm Payment',
children: (context) => [
Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Text('Amount: \$99.99'),
const SizedBox(height: 16),
ButtonWidget.label(
size: const LargeButtonSize(),
color: const BlueButtonColor(),
label: 'Pay with Apple Pay',
onPressed: () {
// Process payment
Navigator.pop(context);
},
),
],
),
),
],
)
// Apple Pay Modal Sheet 02
await CupertinoSheetWidget.showApplePayModalSheet02(
context: context,
title: 'Payment Details',
children: (context) => [/* content */],
)
// Full-screen Apple Pay variants
await CupertinoSheetWidget.showCupertinoModalSheetApplePayModalSheet01(
context: context,
title: 'Payment',
children: (context) => [/* content */],
)
await CupertinoSheetWidget.showCupertinoModalSheet(
context: context,
barrierDismissible: false, // Can't dismiss by tapping outside
enableDrag: false, // Can't swipe to dismiss
title: TitleSheetWidget.standard(
title: 'Loading',
leading: null,
),
children: (context) => [
const Center(
child: Padding(
padding: EdgeInsets.all(32),
child: CupertinoActivityIndicator(),
),
),
],
)
await CupertinoSheetWidget.showModalSheet(
context: context,
colorCallback: (theme) => switch (theme) {
IosLightThemeData() => theme.defaultColors.systemGray06,
IosDarkThemeData() => theme.defaultSystemBackgroundsColors.primaryDarkBase,
},
title: TitleSheetWidget.standard(
title: 'Custom Sheet',
leading: null,
),
children: (context) => [/* content */],
)
Advanced modal with integrated search and prompt messages.
await ModalSheetWidget.showModalSheet(
context: context,
barrierFilter: BarrierFilter.enabled, // Blur background
title: ModalTitleWidget(
title: 'Select Contact',
leftLabelButton: LabelButtonWidget.label(
label: 'Cancel',
onPressed: () => Navigator.pop(context),
),
rightLabelButton: null,
),
cupertinoSearchTextFieldWidget: CupertinoSearchTextFieldWidget(
placeholder: 'Search contacts',
onChanged: (value) {
// Filter list
},
),
prompt: null,
)
await ModalSheetWidget.showModalSheet(
context: context,
prompt: const PromptWidget(
message: 'Select one or more items from the list below.',
),
title: ModalTitleWidget(
title: 'Choose Items',
leftLabelButton: LabelButtonWidget.label(
label: 'Cancel',
onPressed: () => Navigator.pop(context),
),
rightLabelButton: LabelButtonWidget.label(
label: 'Done',
onPressed: () {
// Save selection
Navigator.pop(context);
},
),
),
cupertinoSearchTextFieldWidget: null,
)
await ModalSheetWidget.showModalSheet(
context: context,
barrierFilter: BarrierFilter.enabled,
prompt: const PromptWidget(
message: 'Search for contacts to add to your group.',
),
title: ModalTitleWidget(
title: 'Add Members',
leftLabelButton: LabelButtonWidget.label(
label: 'Back',
iconLeft: CupertinoIcons.back,
onPressed: () => Navigator.pop(context),
),
rightLabelButton: LabelButtonWidget.label(
label: 'Add',
iconRight: CupertinoIcons.add,
onPressed: () {
// Add members
},
),
),
cupertinoSearchTextFieldWidget: CupertinoSearchTextFieldWidget(
onChanged: (value) {
// Filter contacts
},
),
)
class SelectableListModal extends HookWidget {
final List<String> items;
const SelectableListModal({
required this.items,
super.key,
});
static Future<String?> show(BuildContext context, List<String> items) {
return ModalSheetWidget.showModalSheet<String>(
context: context,
barrierFilter: BarrierFilter.enabled,
title: ModalTitleWidget(
title: 'Select Item',
leftLabelButton: LabelButtonWidget.label(
label: 'Cancel',
onPressed: () => Navigator.pop(context),
),
rightLabelButton: null,
),
cupertinoSearchTextFieldWidget: const CupertinoSearchTextFieldWidget(),
prompt: null,
);
}
@override
Widget build(BuildContext context) {
final query = useState('');
final filteredItems = useMemoized(
() => items
.where((item) => item.toLowerCase().contains(query.value.toLowerCase()))
.toList(),
[query.value, items],
);
return ListView.builder(
shrinkWrap: true,
itemCount: filteredItems.length,
itemBuilder: (context, index) {
return RowWidget.standard(
title: filteredItems[index],
description: null,
leftWidget: null,
rightWidget: null,
displayDivider: index < filteredItems.length - 1,
onPressed: () => Navigator.pop(context, filteredItems[index]),
onLongPress: null,
decorationCallback: null,
);
},
);
}
}
// Usage
final selected = await SelectableListModal.show(
context,
['Apple', 'Banana', 'Cherry'],
);
Title header for sheets with leading widget and separator.
// Standard title with close button
TitleSheetWidget.standard(
title: 'Options',
leading: CloseButtonWidget(
onPressed: () => Navigator.pop(context),
),
separator: const DividerWidget(),
)
// Title without leading widget
TitleSheetWidget.standard(
title: 'Select One',
leading: null,
separator: const DividerWidget(),
)
// Apple Pay styled title
TitleSheetWidget.applePay01(
title: 'Confirm Purchase',
leading: CloseButtonWidget(
onPressed: () => Navigator.pop(context),
),
separator: DividerWidget.applePay,
)
// Apple Pay variant 2
TitleSheetWidget.applePay02(
title: 'Payment Method',
leading: CloseButtonWidget(
onPressed: () => Navigator.pop(context),
),
separator: DividerWidget.applePay,
)
final action = await CupertinoSheetWidget.showModalSheet<String>(
context: context,
title: null,
children: (context) => [
RowWidget.standard(
title: 'Share',
description: null,
leftWidget: IconWidget.background(
iconData: CupertinoIcons.share,
),
rightWidget: null,
displayDivider: true,
onPressed: () => Navigator.pop(context, 'share'),
onLongPress: null,
decorationCallback: null,
),
RowWidget.standard(
title: 'Save',
description: null,
leftWidget: IconWidget.background(
iconData: CupertinoIcons.square_arrow_down,
),
rightWidget: null,
displayDivider: true,
onPressed: () => Navigator.pop(context, 'save'),
onLongPress: null,
decorationCallback: null,
),
RowWidget.standard(
title: 'Delete',
description: null,
leftWidget: IconWidget.background(
iconData: CupertinoIcons.trash,
backgroundColorCallback: (theme) => theme.defaultColors.systemRed,
),
rightWidget: null,
displayDivider: false,
onPressed: () => Navigator.pop(context, 'delete'),
onLongPress: null,
decorationCallback: null,
),
const SizedBox(height: 8),
RowWidget.standard(
title: 'Cancel',
description: null,
leftWidget: null,
rightWidget: null,
displayDivider: false,
onPressed: () => Navigator.pop(context),
onLongPress: null,
decorationCallback: null,
),
],
);
if (action == 'delete') {
// Handle delete
}
final confirmed = await CupertinoSheetWidget.showStandardModalSheet<bool>(
context: context,
title: 'Confirm Action',
leading: null,
children: (context) => [
const Padding(
padding: EdgeInsets.all(16),
child: Text(
'Are you sure you want to continue? This action cannot be undone.',
textAlign: TextAlign.center,
),
),
const DividerWidget(),
RowWidget.standard(
title: 'Continue',
description: null,
leftWidget: null,
rightWidget: null,
displayDivider: true,
onPressed: () => Navigator.pop(context, true),
onLongPress: null,
decorationCallback: null,
),
RowWidget.standard(
title: 'Cancel',
description: null,
leftWidget: null,
rightWidget: null,
displayDivider: false,
onPressed: () => Navigator.pop(context, false),
onLongPress: null,
decorationCallback: null,
),
],
);
if (confirmed == true) {
// Proceed with action
}
// Show loading
showCupertinoModalPopup(
context: context,
barrierDismissible: false,
builder: (context) {
final theme = IosTheme.of(context);
return Container(
decoration: BoxDecoration(
color: theme.defaultSystemBackgroundsColors.secondaryDarkBase,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(10),
),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CupertinoActivityIndicator(radius: 20),
const SizedBox(height: 16),
Text(
'Loading...',
style: theme.typography.bodyRegular,
),
],
),
),
),
);
},
);
// Dismiss after operation
await Future.delayed(const Duration(seconds: 2));
Navigator.pop(context);
class MultiSelectModal extends HookWidget {
final List<String> options;
const MultiSelectModal({
required this.options,
super.key,
});
static Future<Set<String>?> show(
BuildContext context,
List<String> options,
) {
return ModalSheetWidget.showModalSheet<Set<String>>(
context: context,
barrierFilter: BarrierFilter.enabled,
prompt: const PromptWidget(
message: 'Select one or more options',
),
title: ModalTitleWidget(
title: 'Choose Options',
leftLabelButton: LabelButtonWidget.label(
label: 'Cancel',
onPressed: () => Navigator.pop(context),
),
rightLabelButton: null,
),
cupertinoSearchTextFieldWidget: null,
);
}
@override
Widget build(BuildContext context) {
final selected = useState<Set<String>>({});
return Column(
children: [
Expanded(
child: ListView.builder(
itemCount: options.length,
itemBuilder: (context, index) {
final option = options[index];
final isSelected = selected.value.contains(option);
return RowWidget.standard(
title: option,
description: null,
leftWidget: null,
rightWidget: isSelected
? Icon(
CupertinoIcons.checkmark_circle_fill,
color: IosTheme.of(context).defaultColors.systemBlue,
)
: const Icon(CupertinoIcons.circle),
displayDivider: index < options.length - 1,
onPressed: () {
final newSelected = Set<String>.from(selected.value);
if (isSelected) {
newSelected.remove(option);
} else {
newSelected.add(option);
}
selected.value = newSelected;
},
onLongPress: null,
decorationCallback: null,
);
},
),
),
ToolBarWidget(
child: ButtonWidget.label(
size: const LargeButtonSize(),
color: const BlueButtonColor(),
label: 'Done (${selected.value.length})',
onPressed: () => Navigator.pop(context, selected.value),
),
),
],
);
}
}
// Usage
final selected = await MultiSelectModal.show(
context,
['Option 1', 'Option 2', 'Option 3'],
);
if (selected != null) {
print('Selected: $selected');
}
Standard iOS close button (X in circle).
CloseButtonWidget(
onPressed: () => Navigator.pop(context),
)
Message displayed at top of modal.
const PromptWidget(
message: 'Select one or more items from the list below.',
)
Title bar with left and right buttons.
ModalTitleWidget(
title: 'Select Contact',
leftLabelButton: LabelButtonWidget.label(
label: 'Cancel',
onPressed: () => Navigator.pop(context),
),
rightLabelButton: LabelButtonWidget.label(
label: 'Done',
onPressed: () {},
),
)
systemWhitesecondaryDarkBaseNavigator.pop(context, value)enableDrag is truebarrierDismissible is not falseMediaQuery.viewInsetsOf(context).bottomonChanged callback updates statesetState or hook state update is calledbarrierFilter to BarrierFilter.enabledfilter parameter is set to kImageFilterBlur/lib/src/widgets/cupertino_sheet_widget.dart/lib/src/widgets/modal_sheet_widget.dart/lib/src/widgets/title_sheet_widget.dart/lib/src/widgets/modal_title_widget.dart/lib/src/widgets/close_button_widget.dart/lib/src/widgets/prompt_widget.dart