← Back to Blog

Flutter CupertinoButton Localization: iOS-Style Buttons for Multilingual Apps

fluttercupertinobuttonioslocalizationrtl

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

  1. Use CupertinoButton.filled for primary translated actions and plain CupertinoButton for secondary actions, matching iOS conventions.

  2. Avoid fixed-width buttons -- let CupertinoButton size to its translated content, or use SizedBox(width: double.infinity) for full-width.

  3. Provide Semantics labels for icon-only CupertinoButtons so VoiceOver reads the translated action name.

  4. Use CupertinoColors.destructiveRed for delete/discard actions with translated confirmation dialogs via CupertinoAlertDialog.

  5. Show CupertinoActivityIndicator during async operations with the button disabled, replacing translated text with a spinner.

  6. 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.

Further Reading