← Back to Blog

Flutter Spacer Localization: Flexible Spacing for Multilingual Layouts

flutterspacerflexspacinglocalizationlayout

Flutter Spacer Localization: Flexible Spacing for Multilingual Layouts

Spacer is Flutter's widget for creating flexible empty space in Flex containers like Row and Column. In multilingual applications, Spacer becomes essential for creating layouts that adapt gracefully to varying text lengths while maintaining proper alignment and spacing.

Understanding Spacer in Localization Context

Spacer expands to fill available space in a Flex container, pushing other widgets apart. For multilingual apps, this is valuable because:

  • Text lengths vary dramatically between languages
  • Flexible spacing adapts to content without overflow
  • RTL layouts work seamlessly with Spacer
  • UI elements maintain proper distribution regardless of translation length

Why Spacer Matters for Multilingual Apps

Proper use of Spacer ensures:

  • Adaptive layouts: Content spreads evenly regardless of text length
  • RTL compatibility: Spacer works identically in both directions
  • Visual balance: Elements maintain pleasing distribution
  • Overflow prevention: Flexible spacing prevents layout breaks

Basic Spacer Implementation

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

class LocalizedSpacerExample extends StatelessWidget {
  const LocalizedSpacerExample({super.key});

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

    return Padding(
      padding: const EdgeInsets.all(16),
      child: Row(
        children: [
          Text(
            l10n.welcomeLabel,
            style: Theme.of(context).textTheme.titleMedium,
          ),
          const Spacer(),
          ElevatedButton(
            onPressed: () {},
            child: Text(l10n.actionButton),
          ),
        ],
      ),
    );
  }
}

Header with Spacer

Localized App Header

class LocalizedHeader extends StatelessWidget {
  final String title;
  final List<Widget>? actions;

  const LocalizedHeader({
    super.key,
    required this.title,
    this.actions,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
      decoration: BoxDecoration(
        color: Theme.of(context).colorScheme.surface,
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.05),
            blurRadius: 4,
            offset: const Offset(0, 2),
          ),
        ],
      ),
      child: Row(
        children: [
          Text(
            title,
            style: Theme.of(context).textTheme.titleLarge?.copyWith(
              fontWeight: FontWeight.bold,
            ),
          ),
          const Spacer(),
          if (actions != null) ...actions!,
        ],
      ),
    );
  }
}

// Usage
class HeaderExample extends StatelessWidget {
  const HeaderExample({super.key});

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

    return LocalizedHeader(
      title: l10n.dashboardTitle,
      actions: [
        IconButton(
          icon: const Icon(Icons.search),
          onPressed: () {},
          tooltip: l10n.searchTooltip,
        ),
        IconButton(
          icon: const Icon(Icons.notifications_outlined),
          onPressed: () {},
          tooltip: l10n.notificationsTooltip,
        ),
      ],
    );
  }
}

List Item with Spacer

Balanced List Items

class LocalizedListItem extends StatelessWidget {
  final IconData icon;
  final String title;
  final String? trailing;
  final VoidCallback? onTap;

  const LocalizedListItem({
    super.key,
    required this.icon,
    required this.title,
    this.trailing,
    this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    return InkWell(
      onTap: onTap,
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
        child: Row(
          children: [
            Icon(
              icon,
              color: Theme.of(context).colorScheme.primary,
            ),
            const SizedBox(width: 16),
            Text(
              title,
              style: Theme.of(context).textTheme.bodyLarge,
            ),
            const Spacer(),
            if (trailing != null)
              Text(
                trailing!,
                style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                  color: Theme.of(context).colorScheme.outline,
                ),
              ),
            const SizedBox(width: 8),
            Icon(
              Icons.chevron_right,
              color: Theme.of(context).colorScheme.outline,
            ),
          ],
        ),
      ),
    );
  }
}

// Usage
class SettingsMenu extends StatelessWidget {
  const SettingsMenu({super.key});

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

    return Column(
      children: [
        LocalizedListItem(
          icon: Icons.language,
          title: l10n.languageSetting,
          trailing: l10n.currentLanguage,
          onTap: () {},
        ),
        LocalizedListItem(
          icon: Icons.notifications,
          title: l10n.notificationsSetting,
          trailing: l10n.enabledStatus,
          onTap: () {},
        ),
        LocalizedListItem(
          icon: Icons.dark_mode,
          title: l10n.themeSetting,
          trailing: l10n.systemDefault,
          onTap: () {},
        ),
      ],
    );
  }
}

Flex Distribution with Multiple Spacers

Evenly Distributed Actions

class DistributedActions extends StatelessWidget {
  const DistributedActions({super.key});

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

    return Padding(
      padding: const EdgeInsets.all(16),
      child: Row(
        children: [
          _ActionButton(
            icon: Icons.share,
            label: l10n.shareAction,
            onPressed: () {},
          ),
          const Spacer(),
          _ActionButton(
            icon: Icons.bookmark_outline,
            label: l10n.saveAction,
            onPressed: () {},
          ),
          const Spacer(),
          _ActionButton(
            icon: Icons.download,
            label: l10n.downloadAction,
            onPressed: () {},
          ),
        ],
      ),
    );
  }
}

class _ActionButton extends StatelessWidget {
  final IconData icon;
  final String label;
  final VoidCallback onPressed;

  const _ActionButton({
    required this.icon,
    required this.label,
    required this.onPressed,
  });

  @override
  Widget build(BuildContext context) {
    return InkWell(
      onTap: onPressed,
      borderRadius: BorderRadius.circular(8),
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(icon),
            const SizedBox(height: 4),
            Text(
              label,
              style: Theme.of(context).textTheme.bodySmall,
            ),
          ],
        ),
      ),
    );
  }
}

Weighted Spacing with Flex

Proportional Space Distribution

class WeightedSpacingExample extends StatelessWidget {
  const WeightedSpacingExample({super.key});

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

    return Padding(
      padding: const EdgeInsets.all(16),
      child: Row(
        children: [
          // Label takes minimum space needed
          Text(l10n.statusLabel),
          // Small space after label
          const Spacer(flex: 1),
          // Status indicator
          Container(
            padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
            decoration: BoxDecoration(
              color: Colors.green.withOpacity(0.1),
              borderRadius: BorderRadius.circular(12),
            ),
            child: Text(
              l10n.activeStatus,
              style: const TextStyle(color: Colors.green),
            ),
          ),
          // Larger space before action
          const Spacer(flex: 2),
          // Action button
          TextButton(
            onPressed: () {},
            child: Text(l10n.viewDetailsButton),
          ),
        ],
      ),
    );
  }
}

Card Footer with Spacer

Localized Card Actions

class LocalizedCardWithFooter extends StatelessWidget {
  final String title;
  final String description;
  final String date;
  final VoidCallback? onPrimary;
  final VoidCallback? onSecondary;

  const LocalizedCardWithFooter({
    super.key,
    required this.title,
    required this.description,
    required this.date,
    this.onPrimary,
    this.onSecondary,
  });

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

    return Card(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  title,
                  style: Theme.of(context).textTheme.titleMedium?.copyWith(
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const SizedBox(height: 8),
                Text(
                  description,
                  style: Theme.of(context).textTheme.bodyMedium,
                ),
              ],
            ),
          ),
          const Divider(height: 1),
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
            child: Row(
              children: [
                Padding(
                  padding: const EdgeInsets.symmetric(horizontal: 8),
                  child: Text(
                    date,
                    style: Theme.of(context).textTheme.bodySmall?.copyWith(
                      color: Theme.of(context).colorScheme.outline,
                    ),
                  ),
                ),
                const Spacer(),
                if (onSecondary != null)
                  TextButton(
                    onPressed: onSecondary,
                    child: Text(l10n.cancelButton),
                  ),
                if (onPrimary != null)
                  TextButton(
                    onPressed: onPrimary,
                    child: Text(l10n.confirmButton),
                  ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

Navigation Bar with Spacer

Flexible Navigation Layout

class LocalizedNavBar extends StatelessWidget {
  final int selectedIndex;
  final ValueChanged<int> onDestinationSelected;

  const LocalizedNavBar({
    super.key,
    required this.selectedIndex,
    required this.onDestinationSelected,
  });

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

    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      decoration: BoxDecoration(
        color: Theme.of(context).colorScheme.surface,
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.1),
            blurRadius: 8,
            offset: const Offset(0, -2),
          ),
        ],
      ),
      child: SafeArea(
        child: Row(
          children: [
            _NavItem(
              icon: Icons.home_outlined,
              selectedIcon: Icons.home,
              label: l10n.navHome,
              isSelected: selectedIndex == 0,
              onTap: () => onDestinationSelected(0),
            ),
            const Spacer(),
            _NavItem(
              icon: Icons.search_outlined,
              selectedIcon: Icons.search,
              label: l10n.navSearch,
              isSelected: selectedIndex == 1,
              onTap: () => onDestinationSelected(1),
            ),
            const Spacer(),
            _NavItem(
              icon: Icons.add_circle_outline,
              selectedIcon: Icons.add_circle,
              label: l10n.navCreate,
              isSelected: selectedIndex == 2,
              onTap: () => onDestinationSelected(2),
            ),
            const Spacer(),
            _NavItem(
              icon: Icons.person_outline,
              selectedIcon: Icons.person,
              label: l10n.navProfile,
              isSelected: selectedIndex == 3,
              onTap: () => onDestinationSelected(3),
            ),
          ],
        ),
      ),
    );
  }
}

class _NavItem extends StatelessWidget {
  final IconData icon;
  final IconData selectedIcon;
  final String label;
  final bool isSelected;
  final VoidCallback onTap;

  const _NavItem({
    required this.icon,
    required this.selectedIcon,
    required this.label,
    required this.isSelected,
    required this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    final color = isSelected
        ? Theme.of(context).colorScheme.primary
        : Theme.of(context).colorScheme.outline;

    return InkWell(
      onTap: onTap,
      borderRadius: BorderRadius.circular(12),
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(
              isSelected ? selectedIcon : icon,
              color: color,
            ),
            const SizedBox(height: 4),
            Text(
              label,
              style: TextStyle(
                color: color,
                fontSize: 12,
                fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Price Row with Spacer

Localized Price Display

class LocalizedPriceRow extends StatelessWidget {
  final String label;
  final String amount;
  final bool isTotal;

  const LocalizedPriceRow({
    super.key,
    required this.label,
    required this.amount,
    this.isTotal = false,
  });

  @override
  Widget build(BuildContext context) {
    final textStyle = isTotal
        ? Theme.of(context).textTheme.titleMedium?.copyWith(
              fontWeight: FontWeight.bold,
            )
        : Theme.of(context).textTheme.bodyMedium;

    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8),
      child: Row(
        children: [
          Text(label, style: textStyle),
          const Spacer(),
          Text(amount, style: textStyle),
        ],
      ),
    );
  }
}

// Usage
class OrderSummary extends StatelessWidget {
  const OrderSummary({super.key});

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

    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              l10n.orderSummaryTitle,
              style: Theme.of(context).textTheme.titleMedium,
            ),
            const SizedBox(height: 16),
            LocalizedPriceRow(
              label: l10n.subtotalLabel,
              amount: l10n.subtotalAmount,
            ),
            LocalizedPriceRow(
              label: l10n.shippingLabel,
              amount: l10n.shippingAmount,
            ),
            LocalizedPriceRow(
              label: l10n.taxLabel,
              amount: l10n.taxAmount,
            ),
            const Divider(),
            LocalizedPriceRow(
              label: l10n.totalLabel,
              amount: l10n.totalAmount,
              isTotal: true,
            ),
          ],
        ),
      ),
    );
  }
}

Toolbar with Spacer

Localized Editor Toolbar

class LocalizedToolbar extends StatelessWidget {
  const LocalizedToolbar({super.key});

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

    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
      decoration: BoxDecoration(
        color: Theme.of(context).colorScheme.surfaceContainerHighest,
        borderRadius: BorderRadius.circular(8),
      ),
      child: Row(
        children: [
          _ToolbarButton(
            icon: Icons.format_bold,
            tooltip: l10n.boldTooltip,
            onPressed: () {},
          ),
          _ToolbarButton(
            icon: Icons.format_italic,
            tooltip: l10n.italicTooltip,
            onPressed: () {},
          ),
          _ToolbarButton(
            icon: Icons.format_underlined,
            tooltip: l10n.underlineTooltip,
            onPressed: () {},
          ),
          const VerticalDivider(width: 16),
          _ToolbarButton(
            icon: Icons.format_list_bulleted,
            tooltip: l10n.bulletListTooltip,
            onPressed: () {},
          ),
          _ToolbarButton(
            icon: Icons.format_list_numbered,
            tooltip: l10n.numberedListTooltip,
            onPressed: () {},
          ),
          const Spacer(),
          _ToolbarButton(
            icon: Icons.undo,
            tooltip: l10n.undoTooltip,
            onPressed: () {},
          ),
          _ToolbarButton(
            icon: Icons.redo,
            tooltip: l10n.redoTooltip,
            onPressed: () {},
          ),
        ],
      ),
    );
  }
}

class _ToolbarButton extends StatelessWidget {
  final IconData icon;
  final String tooltip;
  final VoidCallback onPressed;

  const _ToolbarButton({
    required this.icon,
    required this.tooltip,
    required this.onPressed,
  });

  @override
  Widget build(BuildContext context) {
    return IconButton(
      icon: Icon(icon, size: 20),
      tooltip: tooltip,
      onPressed: onPressed,
      visualDensity: VisualDensity.compact,
    );
  }
}

ARB File Structure

English (app_en.arb)

{
  "@@locale": "en",

  "welcomeLabel": "Welcome back!",
  "@welcomeLabel": {
    "description": "Welcome greeting label"
  },

  "actionButton": "Get Started",
  "@actionButton": {
    "description": "Primary action button"
  },

  "dashboardTitle": "Dashboard",
  "searchTooltip": "Search",
  "notificationsTooltip": "Notifications",

  "languageSetting": "Language",
  "currentLanguage": "English",
  "notificationsSetting": "Notifications",
  "enabledStatus": "Enabled",
  "themeSetting": "Theme",
  "systemDefault": "System",

  "shareAction": "Share",
  "saveAction": "Save",
  "downloadAction": "Download",

  "statusLabel": "Status",
  "activeStatus": "Active",
  "viewDetailsButton": "View Details",

  "cancelButton": "Cancel",
  "confirmButton": "Confirm",

  "navHome": "Home",
  "navSearch": "Search",
  "navCreate": "Create",
  "navProfile": "Profile",

  "orderSummaryTitle": "Order Summary",
  "subtotalLabel": "Subtotal",
  "subtotalAmount": "$99.00",
  "shippingLabel": "Shipping",
  "shippingAmount": "$5.99",
  "taxLabel": "Tax",
  "taxAmount": "$8.40",
  "totalLabel": "Total",
  "totalAmount": "$113.39",

  "boldTooltip": "Bold",
  "italicTooltip": "Italic",
  "underlineTooltip": "Underline",
  "bulletListTooltip": "Bullet List",
  "numberedListTooltip": "Numbered List",
  "undoTooltip": "Undo",
  "redoTooltip": "Redo"
}

German (app_de.arb)

{
  "@@locale": "de",

  "welcomeLabel": "Willkommen zurück!",
  "actionButton": "Loslegen",

  "dashboardTitle": "Übersicht",
  "searchTooltip": "Suchen",
  "notificationsTooltip": "Benachrichtigungen",

  "languageSetting": "Sprache",
  "currentLanguage": "Deutsch",
  "notificationsSetting": "Benachrichtigungen",
  "enabledStatus": "Aktiviert",
  "themeSetting": "Design",
  "systemDefault": "System",

  "shareAction": "Teilen",
  "saveAction": "Speichern",
  "downloadAction": "Herunterladen",

  "statusLabel": "Status",
  "activeStatus": "Aktiv",
  "viewDetailsButton": "Details anzeigen",

  "cancelButton": "Abbrechen",
  "confirmButton": "Bestätigen",

  "navHome": "Startseite",
  "navSearch": "Suchen",
  "navCreate": "Erstellen",
  "navProfile": "Profil",

  "orderSummaryTitle": "Bestellübersicht",
  "subtotalLabel": "Zwischensumme",
  "subtotalAmount": "99,00 €",
  "shippingLabel": "Versand",
  "shippingAmount": "5,99 €",
  "taxLabel": "MwSt.",
  "taxAmount": "8,40 €",
  "totalLabel": "Gesamt",
  "totalAmount": "113,39 €",

  "boldTooltip": "Fett",
  "italicTooltip": "Kursiv",
  "underlineTooltip": "Unterstrichen",
  "bulletListTooltip": "Aufzählung",
  "numberedListTooltip": "Nummerierte Liste",
  "undoTooltip": "Rückgängig",
  "redoTooltip": "Wiederholen"
}

Arabic (app_ar.arb)

{
  "@@locale": "ar",

  "welcomeLabel": "مرحباً بعودتك!",
  "actionButton": "ابدأ الآن",

  "dashboardTitle": "لوحة التحكم",
  "searchTooltip": "بحث",
  "notificationsTooltip": "الإشعارات",

  "languageSetting": "اللغة",
  "currentLanguage": "العربية",
  "notificationsSetting": "الإشعارات",
  "enabledStatus": "مفعّل",
  "themeSetting": "المظهر",
  "systemDefault": "النظام",

  "shareAction": "مشاركة",
  "saveAction": "حفظ",
  "downloadAction": "تنزيل",

  "statusLabel": "الحالة",
  "activeStatus": "نشط",
  "viewDetailsButton": "عرض التفاصيل",

  "cancelButton": "إلغاء",
  "confirmButton": "تأكيد",

  "navHome": "الرئيسية",
  "navSearch": "بحث",
  "navCreate": "إنشاء",
  "navProfile": "الملف الشخصي",

  "orderSummaryTitle": "ملخص الطلب",
  "subtotalLabel": "المجموع الفرعي",
  "subtotalAmount": "٩٩٫٠٠ دولار",
  "shippingLabel": "الشحن",
  "shippingAmount": "٥٫٩٩ دولار",
  "taxLabel": "الضريبة",
  "taxAmount": "٨٫٤٠ دولار",
  "totalLabel": "الإجمالي",
  "totalAmount": "١١٣٫٣٩ دولار",

  "boldTooltip": "عريض",
  "italicTooltip": "مائل",
  "underlineTooltip": "تسطير",
  "bulletListTooltip": "قائمة نقطية",
  "numberedListTooltip": "قائمة مرقمة",
  "undoTooltip": "تراجع",
  "redoTooltip": "إعادة"
}

Best Practices Summary

Do's

  1. Use Spacer for flexible gaps instead of fixed SizedBox when possible
  2. Combine with Expanded for complex flex distributions
  3. Use flex parameter for proportional spacing
  4. Test with long translations to verify layout adaptation
  5. Trust Spacer in RTL - it works identically in both directions

Don'ts

  1. Don't use Spacer outside Flex containers - it only works in Row/Column
  2. Don't overuse Spacer when fixed spacing is more appropriate
  3. Don't forget minimum widths for content that shouldn't shrink
  4. Don't assume equal distribution - verify with different text lengths

Accessibility Considerations

class AccessibleSpacerLayout extends StatelessWidget {
  const AccessibleSpacerLayout({super.key});

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

    return Semantics(
      container: true,
      child: Row(
        children: [
          Semantics(
            header: true,
            child: Text(l10n.sectionTitle),
          ),
          const Spacer(),
          Semantics(
            button: true,
            label: l10n.actionButtonLabel,
            child: ElevatedButton(
              onPressed: () {},
              child: Text(l10n.actionButton),
            ),
          ),
        ],
      ),
    );
  }
}

Conclusion

Spacer is an elegant solution for creating flexible, adaptive layouts in multilingual Flutter applications. By pushing widgets apart with expandable space, it ensures your UI remains balanced and visually pleasing regardless of text length or language direction. Use Spacer when you need proportional distribution and let Flutter's flex system handle the complexity of different content sizes.

Further Reading