Flutter CupertinoButton Localization: iOS-Style Buttons for Multilingual Apps
CupertinoButton is a Flutter widget that provides an iOS-style button with a fade animation on press. In multilingual applications, CupertinoButton is essential for building iOS-native interfaces with translated button labels, handling text overflow for longer translations, adapting button padding for verbose languages, and maintaining iOS design conventions across all supported locales.
Understanding CupertinoButton in Localization Context
CupertinoButton renders an iOS-style tappable area with configurable padding, color, and press animation. For multilingual apps, this enables:
- Translated button labels that maintain iOS design conventions
- Padding adjustments for languages with longer button text
- Localized tooltip and semantic labels for accessibility
- Consistent press feedback across LTR and RTL layouts
Why CupertinoButton Matters for Multilingual Apps
CupertinoButton provides:
- iOS consistency: Native iOS appearance for translated button labels
- Flexible sizing: Adapts to translation length without breaking layout
- Filled variant: CupertinoButton.filled for primary actions with translated text
- Accessibility: Semantic labels in the active language for VoiceOver
Basic CupertinoButton Implementation
import 'package:flutter/cupertino.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedCupertinoButtonExample extends StatelessWidget {
const LocalizedCupertinoButtonExample({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text(l10n.actionsTitle),
),
child: SafeArea(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CupertinoButton(
onPressed: () {},
child: Text(l10n.viewDetailsButton),
),
const SizedBox(height: 16),
CupertinoButton.filled(
onPressed: () {},
child: Text(l10n.confirmButton),
),
const SizedBox(height: 16),
CupertinoButton(
color: CupertinoColors.destructiveRed,
onPressed: () {},
child: Text(l10n.deleteButton),
),
const SizedBox(height: 16),
CupertinoButton(
onPressed: null,
child: Text(l10n.disabledButton),
),
],
),
),
),
);
}
}
Advanced CupertinoButton Patterns for Localization
Action Sheet with Localized Buttons
iOS action sheets use CupertinoButton-style actions with translated labels and destructive/cancel styling.
class LocalizedCupertinoActionSheet extends StatelessWidget {
const LocalizedCupertinoActionSheet({super.key});
void _showActionSheet(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
showCupertinoModalPopup(
context: context,
builder: (context) {
final sheetL10n = AppLocalizations.of(context)!;
return CupertinoActionSheet(
title: Text(sheetL10n.chooseActionTitle),
message: Text(sheetL10n.chooseActionMessage),
actions: [
CupertinoActionSheetAction(
onPressed: () => Navigator.pop(context),
child: Text(sheetL10n.shareButton),
),
CupertinoActionSheetAction(
onPressed: () => Navigator.pop(context),
child: Text(sheetL10n.duplicateButton),
),
CupertinoActionSheetAction(
isDestructiveAction: true,
onPressed: () => Navigator.pop(context),
child: Text(sheetL10n.deleteButton),
),
],
cancelButton: CupertinoActionSheetAction(
isDefaultAction: true,
onPressed: () => Navigator.pop(context),
child: Text(sheetL10n.cancelButton),
),
);
},
);
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Center(
child: CupertinoButton.filled(
onPressed: () => _showActionSheet(context),
child: Text(l10n.showOptionsButton),
),
);
}
}
Button Group with Localized Labels
A row of CupertinoButtons styled as a segmented action group with translated labels.
class LocalizedButtonGroup extends StatefulWidget {
const LocalizedButtonGroup({super.key});
@override
State<LocalizedButtonGroup> createState() => _LocalizedButtonGroupState();
}
class _LocalizedButtonGroupState extends State<LocalizedButtonGroup> {
bool _isLoading = false;
Future<void> _handleSubmit() async {
setState(() => _isLoading = true);
await Future.delayed(const Duration(seconds: 2));
if (mounted) {
setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Row(
children: [
Expanded(
child: CupertinoButton(
padding: const EdgeInsets.symmetric(vertical: 12),
onPressed: () {},
child: Text(l10n.saveDraftButton),
),
),
const SizedBox(width: 12),
Expanded(
child: CupertinoButton.filled(
padding: const EdgeInsets.symmetric(vertical: 12),
onPressed: _isLoading ? null : _handleSubmit,
child: _isLoading
? const CupertinoActivityIndicator(
color: CupertinoColors.white,
)
: Text(l10n.publishButton),
),
),
],
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: CupertinoButton(
color: CupertinoColors.destructiveRed,
onPressed: () {
showCupertinoDialog(
context: context,
builder: (context) {
final dialogL10n = AppLocalizations.of(context)!;
return CupertinoAlertDialog(
title: Text(dialogL10n.discardDraftTitle),
content: Text(dialogL10n.discardDraftMessage),
actions: [
CupertinoDialogAction(
onPressed: () => Navigator.pop(context),
child: Text(dialogL10n.cancelButton),
),
CupertinoDialogAction(
isDestructiveAction: true,
onPressed: () => Navigator.pop(context),
child: Text(dialogL10n.discardButton),
),
],
);
},
);
},
child: Text(l10n.discardDraftButton),
),
),
],
),
);
}
}
Icon Button with Localized Tooltip
CupertinoButton with an icon and a localized semantic label for accessibility.
class LocalizedCupertinoIconButton extends StatelessWidget {
const LocalizedCupertinoIconButton({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Semantics(
label: l10n.favoriteButtonLabel,
child: CupertinoButton(
padding: EdgeInsets.zero,
onPressed: () {},
child: const Icon(CupertinoIcons.heart),
),
),
Semantics(
label: l10n.shareButtonLabel,
child: CupertinoButton(
padding: EdgeInsets.zero,
onPressed: () {},
child: const Icon(CupertinoIcons.share),
),
),
Semantics(
label: l10n.bookmarkButtonLabel,
child: CupertinoButton(
padding: EdgeInsets.zero,
onPressed: () {},
child: const Icon(CupertinoIcons.bookmark),
),
),
],
);
}
}
RTL Support and Bidirectional Layouts
CupertinoButton content automatically adapts to RTL. Text aligns correctly, and icon+text combinations reverse order based on the ambient directionality.
class BidirectionalCupertinoButton extends StatelessWidget {
const BidirectionalCupertinoButton({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isRtl = Directionality.of(context) == TextDirection.rtl;
return Padding(
padding: const EdgeInsetsDirectional.all(16),
child: Column(
children: [
CupertinoButton.filled(
onPressed: () {},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(l10n.continueButton),
const SizedBox(width: 8),
Icon(
isRtl
? CupertinoIcons.arrow_left
: CupertinoIcons.arrow_right,
size: 18,
),
],
),
),
const SizedBox(height: 16),
CupertinoButton(
onPressed: () {},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isRtl
? CupertinoIcons.arrow_right
: CupertinoIcons.arrow_left,
size: 18,
),
const SizedBox(width: 8),
Text(l10n.goBackButton),
],
),
),
],
),
);
}
}
Testing CupertinoButton Localization
import 'package:flutter/cupertino.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
void main() {
Widget buildTestWidget({Locale locale = const Locale('en')}) {
return CupertinoApp(
locale: locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const LocalizedCupertinoButtonExample(),
);
}
testWidgets('CupertinoButton renders localized text', (tester) async {
await tester.pumpWidget(buildTestWidget());
await tester.pumpAndSettle();
expect(find.byType(CupertinoButton), findsWidgets);
});
testWidgets('CupertinoButton works in RTL', (tester) async {
await tester.pumpWidget(buildTestWidget(locale: const Locale('ar')));
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
});
}
Best Practices
Use
CupertinoButton.filledfor primary translated actions and plainCupertinoButtonfor secondary actions, matching iOS conventions.Avoid fixed-width buttons -- let CupertinoButton size to its translated content, or use
SizedBox(width: double.infinity)for full-width.Provide
Semanticslabels for icon-only CupertinoButtons so VoiceOver reads the translated action name.Use
CupertinoColors.destructiveRedfor delete/discard actions with translated confirmation dialogs viaCupertinoAlertDialog.Show
CupertinoActivityIndicatorduring async operations with the button disabled, replacing translated text with a spinner.Test button layout in RTL to verify icon+text combinations reverse correctly and action sheets display in the right order.
Conclusion
CupertinoButton provides iOS-native button styling for Flutter apps. For multilingual apps, it adapts seamlessly to translated labels of varying length while maintaining the expected iOS press animation and visual style. By combining CupertinoButton with localized action sheets, alert dialogs, semantic labels, and RTL-aware icon placement, you can build iOS interfaces that feel native in every supported language.