← Back to Blog

Flutter Snackbar Localization: Feedback Messages and Action Buttons

fluttersnackbarfeedbacklocalizationnotificationsactions

Flutter Snackbar Localization: Feedback Messages and Action Buttons

Snackbars provide brief feedback about operations at the bottom of the screen. Proper localization of snackbar messages ensures users worldwide understand app feedback clearly. This guide covers everything you need to know about localizing snackbars in Flutter.

Understanding Snackbar Localization

Snackbar localization involves several key elements:

  1. Feedback messages - Success, error, and info notifications
  2. Action buttons - Undo, retry, dismiss actions
  3. Duration timing - Adjusted for message length
  4. Accessibility - Screen reader announcements
  5. RTL support - Right-to-left layout

Setting Up Snackbar Localization

ARB File Structure

{
  "@@locale": "en",

  "snackbarSuccess": "Operation completed successfully",
  "@snackbarSuccess": {
    "description": "Generic success message"
  },

  "snackbarError": "An error occurred. Please try again.",
  "@snackbarError": {
    "description": "Generic error message"
  },

  "snackbarNoInternet": "No internet connection",
  "@snackbarNoInternet": {
    "description": "No internet connection message"
  },

  "snackbarSaved": "Changes saved",
  "@snackbarSaved": {
    "description": "Save confirmation message"
  },

  "snackbarDeleted": "{item} deleted",
  "@snackbarDeleted": {
    "description": "Delete confirmation with item name",
    "placeholders": {
      "item": {"type": "String"}
    }
  },

  "snackbarItemsDeleted": "{count, plural, =1{1 item deleted} other{{count} items deleted}}",
  "@snackbarItemsDeleted": {
    "description": "Multiple items deleted confirmation",
    "placeholders": {
      "count": {"type": "int"}
    }
  },

  "snackbarUndo": "Undo",
  "@snackbarUndo": {
    "description": "Undo action button"
  },

  "snackbarRetry": "Retry",
  "@snackbarRetry": {
    "description": "Retry action button"
  },

  "snackbarDismiss": "Dismiss",
  "@snackbarDismiss": {
    "description": "Dismiss action button"
  },

  "snackbarViewDetails": "View Details",
  "@snackbarViewDetails": {
    "description": "View details action button"
  },

  "snackbarCopied": "Copied to clipboard",
  "@snackbarCopied": {
    "description": "Copy confirmation message"
  },

  "snackbarSent": "Message sent",
  "@snackbarSent": {
    "description": "Message sent confirmation"
  },

  "snackbarAdded": "{item} added to {destination}",
  "@snackbarAdded": {
    "description": "Item added confirmation",
    "placeholders": {
      "item": {"type": "String"},
      "destination": {"type": "String"}
    }
  },

  "snackbarUndoAction": "Action undone",
  "@snackbarUndoAction": {
    "description": "Undo confirmation message"
  },

  "snackbarOfflineMode": "You're offline. Changes will sync when connected.",
  "@snackbarOfflineMode": {
    "description": "Offline mode notification"
  },

  "snackbarSyncing": "Syncing...",
  "@snackbarSyncing": {
    "description": "Sync in progress message"
  },

  "snackbarSyncComplete": "Sync complete",
  "@snackbarSyncComplete": {
    "description": "Sync completed message"
  }
}

French Translations

{
  "@@locale": "fr",

  "snackbarSuccess": "Opération réussie",
  "snackbarError": "Une erreur s'est produite. Veuillez réessayer.",
  "snackbarNoInternet": "Pas de connexion Internet",
  "snackbarSaved": "Modifications enregistrées",
  "snackbarDeleted": "{item} supprimé",
  "snackbarItemsDeleted": "{count, plural, =1{1 élément supprimé} other{{count} éléments supprimés}}",
  "snackbarUndo": "Annuler",
  "snackbarRetry": "Réessayer",
  "snackbarDismiss": "Fermer",
  "snackbarViewDetails": "Voir les détails",
  "snackbarCopied": "Copié dans le presse-papiers",
  "snackbarSent": "Message envoyé",
  "snackbarAdded": "{item} ajouté à {destination}",
  "snackbarUndoAction": "Action annulée",
  "snackbarOfflineMode": "Vous êtes hors ligne. Les modifications seront synchronisées une fois connecté.",
  "snackbarSyncing": "Synchronisation...",
  "snackbarSyncComplete": "Synchronisation terminée"
}

Arabic Translations (RTL)

{
  "@@locale": "ar",

  "snackbarSuccess": "تمت العملية بنجاح",
  "snackbarError": "حدث خطأ. يرجى المحاولة مرة أخرى.",
  "snackbarNoInternet": "لا يوجد اتصال بالإنترنت",
  "snackbarSaved": "تم حفظ التغييرات",
  "snackbarDeleted": "تم حذف {item}",
  "snackbarItemsDeleted": "{count, plural, =0{لم يتم حذف أي عنصر} =1{تم حذف عنصر واحد} two{تم حذف عنصران} few{تم حذف {count} عناصر} many{تم حذف {count} عنصراً} other{تم حذف {count} عنصر}}",
  "snackbarUndo": "تراجع",
  "snackbarRetry": "إعادة المحاولة",
  "snackbarDismiss": "إغلاق",
  "snackbarViewDetails": "عرض التفاصيل",
  "snackbarCopied": "تم النسخ إلى الحافظة",
  "snackbarSent": "تم إرسال الرسالة",
  "snackbarAdded": "تمت إضافة {item} إلى {destination}",
  "snackbarUndoAction": "تم التراجع عن الإجراء",
  "snackbarOfflineMode": "أنت غير متصل. سيتم مزامنة التغييرات عند الاتصال.",
  "snackbarSyncing": "جارٍ المزامنة...",
  "snackbarSyncComplete": "اكتملت المزامنة"
}

Building Localized Snackbar Service

Snackbar Service

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

enum SnackbarType { success, error, info, warning }

class SnackbarService {
  final GlobalKey<ScaffoldMessengerState> messengerKey;

  SnackbarService(this.messengerKey);

  void showSuccess(BuildContext context, String message, {VoidCallback? onUndo}) {
    _show(
      context,
      message: message,
      type: SnackbarType.success,
      onUndo: onUndo,
    );
  }

  void showError(BuildContext context, String message, {VoidCallback? onRetry}) {
    final l10n = AppLocalizations.of(context)!;
    _show(
      context,
      message: message,
      type: SnackbarType.error,
      actionLabel: onRetry != null ? l10n.snackbarRetry : null,
      onAction: onRetry,
      duration: const Duration(seconds: 6),
    );
  }

  void showInfo(BuildContext context, String message) {
    _show(context, message: message, type: SnackbarType.info);
  }

  void showOffline(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;
    _show(
      context,
      message: l10n.snackbarOfflineMode,
      type: SnackbarType.warning,
      duration: const Duration(seconds: 5),
    );
  }

  void showDeleted(
    BuildContext context, {
    required String itemName,
    required VoidCallback onUndo,
  }) {
    final l10n = AppLocalizations.of(context)!;
    _show(
      context,
      message: l10n.snackbarDeleted(itemName),
      type: SnackbarType.info,
      actionLabel: l10n.snackbarUndo,
      onAction: onUndo,
    );
  }

  void showItemsDeleted(
    BuildContext context, {
    required int count,
    required VoidCallback onUndo,
  }) {
    final l10n = AppLocalizations.of(context)!;
    _show(
      context,
      message: l10n.snackbarItemsDeleted(count),
      type: SnackbarType.info,
      actionLabel: l10n.snackbarUndo,
      onAction: onUndo,
    );
  }

  void showCopied(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;
    _show(
      context,
      message: l10n.snackbarCopied,
      type: SnackbarType.success,
      duration: const Duration(seconds: 2),
    );
  }

  void _show(
    BuildContext context, {
    required String message,
    required SnackbarType type,
    String? actionLabel,
    VoidCallback? onAction,
    VoidCallback? onUndo,
    Duration duration = const Duration(seconds: 4),
  }) {
    final l10n = AppLocalizations.of(context)!;
    final theme = Theme.of(context);

    final effectiveActionLabel = onUndo != null ? l10n.snackbarUndo : actionLabel;
    final effectiveOnAction = onUndo ?? onAction;

    final snackBar = SnackBar(
      content: Row(
        children: [
          Icon(
            _getIcon(type),
            color: Colors.white,
            size: 20,
          ),
          const SizedBox(width: 12),
          Expanded(
            child: Text(
              message,
              style: const TextStyle(color: Colors.white),
            ),
          ),
        ],
      ),
      backgroundColor: _getColor(type, theme),
      duration: duration,
      behavior: SnackBarBehavior.floating,
      action: effectiveActionLabel != null && effectiveOnAction != null
          ? SnackBarAction(
              label: effectiveActionLabel,
              textColor: Colors.white,
              onPressed: effectiveOnAction,
            )
          : null,
    );

    messengerKey.currentState
      ?..hideCurrentSnackBar()
      ..showSnackBar(snackBar);
  }

  IconData _getIcon(SnackbarType type) {
    switch (type) {
      case SnackbarType.success:
        return Icons.check_circle;
      case SnackbarType.error:
        return Icons.error;
      case SnackbarType.info:
        return Icons.info;
      case SnackbarType.warning:
        return Icons.warning;
    }
  }

  Color _getColor(SnackbarType type, ThemeData theme) {
    switch (type) {
      case SnackbarType.success:
        return Colors.green.shade700;
      case SnackbarType.error:
        return theme.colorScheme.error;
      case SnackbarType.info:
        return Colors.blue.shade700;
      case SnackbarType.warning:
        return Colors.orange.shade700;
    }
  }
}

Using the Snackbar Service

class MyApp extends StatelessWidget {
  final messengerKey = GlobalKey<ScaffoldMessengerState>();

  MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      scaffoldMessengerKey: messengerKey,
      home: HomePage(snackbarService: SnackbarService(messengerKey)),
    );
  }
}

class HomePage extends StatelessWidget {
  final SnackbarService snackbarService;

  const HomePage({super.key, required this.snackbarService});

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Scaffold(
      appBar: AppBar(title: const Text('Snackbar Demo')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () => snackbarService.showSuccess(
                context,
                l10n.snackbarSaved,
              ),
              child: const Text('Show Success'),
            ),
            ElevatedButton(
              onPressed: () => snackbarService.showError(
                context,
                l10n.snackbarError,
                onRetry: () => print('Retry'),
              ),
              child: const Text('Show Error'),
            ),
            ElevatedButton(
              onPressed: () => snackbarService.showDeleted(
                context,
                itemName: 'Document',
                onUndo: () => print('Undo delete'),
              ),
              child: const Text('Show Delete with Undo'),
            ),
          ],
        ),
      ),
    );
  }
}

Duration Adjustment for Text Length

class AdaptiveDurationSnackbar {
  static Duration calculateDuration(String message, {int minSeconds = 3}) {
    // Average reading speed: ~200 words per minute = ~3.3 words per second
    final wordCount = message.split(' ').length;
    final readingSeconds = (wordCount / 3.3).ceil();
    final duration = max(minSeconds, readingSeconds);
    return Duration(seconds: duration.clamp(minSeconds, 10));
  }

  static void show(
    BuildContext context, {
    required String message,
    SnackBarAction? action,
  }) {
    final duration = calculateDuration(message);

    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(message),
        duration: duration,
        action: action,
      ),
    );
  }
}

Accessibility

Screen Reader Announcements

class AccessibleSnackbar {
  static void show(
    BuildContext context, {
    required String message,
    String? actionLabel,
    VoidCallback? onAction,
  }) {
    // Announce to screen readers
    SemanticsService.announce(
      message,
      Directionality.of(context),
    );

    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Semantics(
          liveRegion: true,
          child: Text(message),
        ),
        action: actionLabel != null && onAction != null
            ? SnackBarAction(
                label: actionLabel,
                onPressed: onAction,
              )
            : null,
      ),
    );
  }
}

Testing Snackbar Localization

void main() {
  group('Snackbar Localization', () {
    testWidgets('displays localized success message', (tester) async {
      await tester.pumpWidget(
        MaterialApp(
          locale: const Locale('fr'),
          localizationsDelegates: AppLocalizations.localizationsDelegates,
          supportedLocales: AppLocalizations.supportedLocales,
          home: Builder(
            builder: (context) {
              return Scaffold(
                body: ElevatedButton(
                  onPressed: () {
                    final l10n = AppLocalizations.of(context)!;
                    ScaffoldMessenger.of(context).showSnackBar(
                      SnackBar(content: Text(l10n.snackbarSaved)),
                    );
                  },
                  child: const Text('Show'),
                ),
              );
            },
          ),
        ),
      );

      await tester.tap(find.text('Show'));
      await tester.pumpAndSettle();

      expect(find.text('Modifications enregistrées'), findsOneWidget);
    });

    testWidgets('displays localized undo button', (tester) async {
      await tester.pumpWidget(
        MaterialApp(
          locale: const Locale('de'),
          localizationsDelegates: AppLocalizations.localizationsDelegates,
          supportedLocales: AppLocalizations.supportedLocales,
          home: Builder(
            builder: (context) {
              return Scaffold(
                body: ElevatedButton(
                  onPressed: () {
                    final l10n = AppLocalizations.of(context)!;
                    ScaffoldMessenger.of(context).showSnackBar(
                      SnackBar(
                        content: Text(l10n.snackbarDeleted('Item')),
                        action: SnackBarAction(
                          label: l10n.snackbarUndo,
                          onPressed: () {},
                        ),
                      ),
                    );
                  },
                  child: const Text('Delete'),
                ),
              );
            },
          ),
        ),
      );

      await tester.tap(find.text('Delete'));
      await tester.pumpAndSettle();

      expect(find.text('Rückgängig'), findsOneWidget);
    });
  });
}

Best Practices

  1. Keep messages concise - Short, clear feedback
  2. Use appropriate duration - Longer messages need more time
  3. Provide undo actions - For destructive operations
  4. Announce to screen readers - Use semantic announcements
  5. Use consistent icons - Visual indicators for message types
  6. Test pluralization - Verify plural forms work correctly

Conclusion

Proper snackbar localization ensures users worldwide understand app feedback clearly. By following these patterns, your Flutter snackbars will communicate effectively in any language.

Additional Resources