← Back to Blog

Flutter FittedBox Localization: Scaling Content for Multilingual Apps

flutterfittedboxscalinglayoutlocalizationtext

Flutter FittedBox Localization: Scaling Content for Multilingual Apps

FittedBox is a Flutter widget that scales and positions its child within itself according to a fit parameter. In multilingual applications, FittedBox helps handle varying text lengths by automatically scaling content to fit available space.

Understanding FittedBox in Localization Context

FittedBox scales its child to fit within the available constraints. For multilingual apps, this creates valuable capabilities:

  • Long translations automatically scale to fit
  • Icons and text maintain proportional relationships
  • RTL content scales correctly
  • Different scripts display at appropriate sizes

Why FittedBox Matters for Multilingual Apps

Automatic scaling ensures:

  • Text accommodation: Long translations don't overflow
  • Visual consistency: Elements maintain proportions across languages
  • Adaptive layouts: Content scales to available space
  • No truncation: Full text remains visible, just smaller

Basic FittedBox Implementation

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

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

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

    return Container(
      width: 200,
      height: 50,
      padding: const EdgeInsets.symmetric(horizontal: 16),
      decoration: BoxDecoration(
        color: Theme.of(context).colorScheme.primaryContainer,
        borderRadius: BorderRadius.circular(8),
      ),
      child: FittedBox(
        fit: BoxFit.scaleDown,
        child: Text(
          l10n.buttonLabel,
          style: Theme.of(context).textTheme.titleMedium,
        ),
      ),
    );
  }
}

Button Text Scaling

Adaptive Button Component

class LocalizedScalingButton extends StatelessWidget {
  final String label;
  final VoidCallback onPressed;
  final IconData? icon;
  final double maxWidth;

  const LocalizedScalingButton({
    super.key,
    required this.label,
    required this.onPressed,
    this.icon,
    this.maxWidth = 200,
  });

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: maxWidth,
      height: 48,
      child: FilledButton(
        onPressed: onPressed,
        child: FittedBox(
          fit: BoxFit.scaleDown,
          child: Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              if (icon != null) ...[
                Icon(icon, size: 20),
                const SizedBox(width: 8),
              ],
              Text(label),
            ],
          ),
        ),
      ),
    );
  }
}

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

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

    return Column(
      children: [
        LocalizedScalingButton(
          label: l10n.submitButton,
          icon: Icons.check,
          onPressed: () {},
        ),
        const SizedBox(height: 16),
        LocalizedScalingButton(
          label: l10n.cancelSubscriptionButton, // Longer text
          icon: Icons.cancel,
          onPressed: () {},
        ),
      ],
    );
  }
}

Button Row with Scaling

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

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

    return Row(
      children: [
        Expanded(
          child: SizedBox(
            height: 48,
            child: OutlinedButton(
              onPressed: () {},
              child: FittedBox(
                fit: BoxFit.scaleDown,
                child: Text(l10n.cancelButton),
              ),
            ),
          ),
        ),
        const SizedBox(width: 16),
        Expanded(
          child: SizedBox(
            height: 48,
            child: FilledButton(
              onPressed: () {},
              child: FittedBox(
                fit: BoxFit.scaleDown,
                child: Text(l10n.confirmButton),
              ),
            ),
          ),
        ),
      ],
    );
  }
}

Header and Title Scaling

Scaling Page Title

class LocalizedScalingTitle extends StatelessWidget {
  final String title;
  final TextStyle? style;

  const LocalizedScalingTitle({
    super.key,
    required this.title,
    this.style,
  });

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: 60,
      child: FittedBox(
        fit: BoxFit.scaleDown,
        alignment: AlignmentDirectional.centerStart,
        child: Text(
          title,
          style: style ?? Theme.of(context).textTheme.headlineLarge,
        ),
      ),
    );
  }
}

// Page with scaling title
class LocalizedPage extends StatelessWidget {
  const LocalizedPage({super.key});

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

    return Scaffold(
      body: SafeArea(
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              LocalizedScalingTitle(
                title: l10n.pageTitle,
              ),
              const SizedBox(height: 16),
              Text(
                l10n.pageDescription,
                style: Theme.of(context).textTheme.bodyMedium,
              ),
            ],
          ),
        ),
      ),
    );
  }
}

AppBar Title Scaling

class ScalingAppBar extends StatelessWidget implements PreferredSizeWidget {
  final String title;
  final List<Widget>? actions;

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

  @override
  Size get preferredSize => const Size.fromHeight(kToolbarHeight);

  @override
  Widget build(BuildContext context) {
    return AppBar(
      title: FittedBox(
        fit: BoxFit.scaleDown,
        child: Text(title),
      ),
      actions: actions,
    );
  }
}

Card and Tile Scaling

Product Card with Scaling Labels

class LocalizedProductCard extends StatelessWidget {
  final String imageUrl;
  final String name;
  final String price;
  final String? badge;
  final VoidCallback onTap;

  const LocalizedProductCard({
    super.key,
    required this.imageUrl,
    required this.name,
    required this.price,
    this.badge,
    required this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      clipBehavior: Clip.antiAlias,
      child: InkWell(
        onTap: onTap,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            AspectRatio(
              aspectRatio: 1,
              child: Stack(
                fit: StackFit.expand,
                children: [
                  Image.network(
                    imageUrl,
                    fit: BoxFit.cover,
                  ),
                  if (badge != null)
                    PositionedDirectional(
                      top: 8,
                      start: 8,
                      child: Container(
                        constraints: const BoxConstraints(maxWidth: 80),
                        padding: const EdgeInsets.symmetric(
                          horizontal: 8,
                          vertical: 4,
                        ),
                        decoration: BoxDecoration(
                          color: Colors.red,
                          borderRadius: BorderRadius.circular(4),
                        ),
                        child: FittedBox(
                          fit: BoxFit.scaleDown,
                          child: Text(
                            badge!,
                            style: const TextStyle(
                              color: Colors.white,
                              fontWeight: FontWeight.bold,
                            ),
                          ),
                        ),
                      ),
                    ),
                ],
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(12),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  SizedBox(
                    height: 40,
                    child: FittedBox(
                      fit: BoxFit.scaleDown,
                      alignment: AlignmentDirectional.centerStart,
                      child: Text(
                        name,
                        style: Theme.of(context).textTheme.titleSmall?.copyWith(
                          fontWeight: FontWeight.w600,
                        ),
                      ),
                    ),
                  ),
                  const SizedBox(height: 4),
                  Text(
                    price,
                    style: Theme.of(context).textTheme.titleMedium?.copyWith(
                      color: Theme.of(context).colorScheme.primary,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Tab Labels with Scaling

Scaling Tab Bar

class LocalizedScalingTabBar extends StatelessWidget {
  final List<String> labels;
  final TabController controller;

  const LocalizedScalingTabBar({
    super.key,
    required this.labels,
    required this.controller,
  });

  @override
  Widget build(BuildContext context) {
    return TabBar(
      controller: controller,
      tabs: labels.map((label) {
        return Tab(
          child: FittedBox(
            fit: BoxFit.scaleDown,
            child: Text(label),
          ),
        );
      }).toList(),
    );
  }
}

// Usage
class TabExample extends StatefulWidget {
  const TabExample({super.key});

  @override
  State<TabExample> createState() => _TabExampleState();
}

class _TabExampleState extends State<TabExample> with SingleTickerProviderStateMixin {
  late TabController _controller;

  @override
  void initState() {
    super.initState();
    _controller = TabController(length: 4, vsync: this);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

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

    return Column(
      children: [
        LocalizedScalingTabBar(
          controller: _controller,
          labels: [
            l10n.tabOverview,
            l10n.tabDetails,
            l10n.tabReviews,
            l10n.tabRelated,
          ],
        ),
        Expanded(
          child: TabBarView(
            controller: _controller,
            children: const [
              Center(child: Text('Overview')),
              Center(child: Text('Details')),
              Center(child: Text('Reviews')),
              Center(child: Text('Related')),
            ],
          ),
        ),
      ],
    );
  }
}

Navigation Items

Scaling Bottom Navigation

class LocalizedBottomNavigation extends StatelessWidget {
  final int currentIndex;
  final ValueChanged<int> onTap;

  const LocalizedBottomNavigation({
    super.key,
    required this.currentIndex,
    required this.onTap,
  });

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

    return BottomNavigationBar(
      currentIndex: currentIndex,
      onTap: onTap,
      type: BottomNavigationBarType.fixed,
      items: [
        _buildNavItem(Icons.home, l10n.navHome),
        _buildNavItem(Icons.search, l10n.navSearch),
        _buildNavItem(Icons.favorite, l10n.navFavorites),
        _buildNavItem(Icons.person, l10n.navProfile),
      ],
    );
  }

  BottomNavigationBarItem _buildNavItem(IconData icon, String label) {
    return BottomNavigationBarItem(
      icon: Icon(icon),
      label: label,
      // Note: For very long labels, consider using a custom bottom nav
    );
  }
}

// Custom bottom nav with FittedBox for labels
class CustomScalingBottomNav extends StatelessWidget {
  final int currentIndex;
  final ValueChanged<int> onTap;

  const CustomScalingBottomNav({
    super.key,
    required this.currentIndex,
    required this.onTap,
  });

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

    final items = [
      _NavItem(Icons.home, l10n.navHome),
      _NavItem(Icons.search, l10n.navSearch),
      _NavItem(Icons.favorite, l10n.navFavorites),
      _NavItem(Icons.person, l10n.navProfile),
    ];

    return Container(
      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: Padding(
          padding: const EdgeInsets.symmetric(vertical: 8),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: List.generate(items.length, (index) {
              final item = items[index];
              final isSelected = index == currentIndex;

              return Expanded(
                child: InkWell(
                  onTap: () => onTap(index),
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      Icon(
                        item.icon,
                        color: isSelected
                            ? Theme.of(context).colorScheme.primary
                            : Theme.of(context).colorScheme.outline,
                      ),
                      const SizedBox(height: 4),
                      SizedBox(
                        width: 70,
                        child: FittedBox(
                          fit: BoxFit.scaleDown,
                          child: Text(
                            item.label,
                            style: TextStyle(
                              fontSize: 12,
                              color: isSelected
                                  ? Theme.of(context).colorScheme.primary
                                  : Theme.of(context).colorScheme.outline,
                            ),
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
              );
            }),
          ),
        ),
      ),
    );
  }
}

class _NavItem {
  final IconData icon;
  final String label;

  _NavItem(this.icon, this.label);
}

Statistics and Metrics Display

Scaling Stat Cards

class LocalizedStatCard extends StatelessWidget {
  final String value;
  final String label;
  final IconData icon;
  final Color? color;

  const LocalizedStatCard({
    super.key,
    required this.value,
    required this.label,
    required this.icon,
    this.color,
  });

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

    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(
              icon,
              size: 32,
              color: cardColor,
            ),
            const SizedBox(height: 8),
            FittedBox(
              fit: BoxFit.scaleDown,
              child: Text(
                value,
                style: Theme.of(context).textTheme.headlineMedium?.copyWith(
                  fontWeight: FontWeight.bold,
                  color: cardColor,
                ),
              ),
            ),
            const SizedBox(height: 4),
            SizedBox(
              height: 36,
              child: FittedBox(
                fit: BoxFit.scaleDown,
                child: Text(
                  label,
                  style: Theme.of(context).textTheme.bodySmall?.copyWith(
                    color: Theme.of(context).colorScheme.outline,
                  ),
                  textAlign: TextAlign.center,
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

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

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

    return GridView.count(
      shrinkWrap: true,
      crossAxisCount: 2,
      childAspectRatio: 1.2,
      padding: const EdgeInsets.all(16),
      mainAxisSpacing: 16,
      crossAxisSpacing: 16,
      children: [
        LocalizedStatCard(
          value: '1,234',
          label: l10n.statUsers,
          icon: Icons.people,
        ),
        LocalizedStatCard(
          value: '56.7K',
          label: l10n.statDownloads,
          icon: Icons.download,
          color: Colors.green,
        ),
        LocalizedStatCard(
          value: '98.5%',
          label: l10n.statSatisfaction,
          icon: Icons.thumb_up,
          color: Colors.orange,
        ),
        LocalizedStatCard(
          value: '4.8',
          label: l10n.statRating,
          icon: Icons.star,
          color: Colors.amber,
        ),
      ],
    );
  }
}

ARB File Structure

English (app_en.arb)

{
  "@@locale": "en",

  "buttonLabel": "Submit",
  "@buttonLabel": {
    "description": "Generic submit button"
  },

  "submitButton": "Submit",
  "cancelSubscriptionButton": "Cancel Subscription",
  "cancelButton": "Cancel",
  "confirmButton": "Confirm",

  "pageTitle": "Dashboard Overview",
  "@pageTitle": {
    "description": "Dashboard page title"
  },

  "pageDescription": "View your analytics and performance metrics here.",

  "tabOverview": "Overview",
  "tabDetails": "Details",
  "tabReviews": "Reviews",
  "tabRelated": "Related",

  "navHome": "Home",
  "navSearch": "Search",
  "navFavorites": "Favorites",
  "navProfile": "Profile",

  "statUsers": "Active Users",
  "statDownloads": "Downloads",
  "statSatisfaction": "Satisfaction",
  "statRating": "Rating"
}

German (app_de.arb)

{
  "@@locale": "de",

  "buttonLabel": "Absenden",
  "submitButton": "Absenden",
  "cancelSubscriptionButton": "Abonnement kündigen",
  "cancelButton": "Abbrechen",
  "confirmButton": "Bestätigen",

  "pageTitle": "Dashboard-Übersicht",
  "pageDescription": "Sehen Sie hier Ihre Analysen und Leistungskennzahlen.",

  "tabOverview": "Übersicht",
  "tabDetails": "Details",
  "tabReviews": "Bewertungen",
  "tabRelated": "Verwandt",

  "navHome": "Startseite",
  "navSearch": "Suche",
  "navFavorites": "Favoriten",
  "navProfile": "Profil",

  "statUsers": "Aktive Benutzer",
  "statDownloads": "Downloads",
  "statSatisfaction": "Zufriedenheit",
  "statRating": "Bewertung"
}

Arabic (app_ar.arb)

{
  "@@locale": "ar",

  "buttonLabel": "إرسال",
  "submitButton": "إرسال",
  "cancelSubscriptionButton": "إلغاء الاشتراك",
  "cancelButton": "إلغاء",
  "confirmButton": "تأكيد",

  "pageTitle": "نظرة عامة على لوحة التحكم",
  "pageDescription": "اعرض تحليلاتك ومقاييس الأداء هنا.",

  "tabOverview": "نظرة عامة",
  "tabDetails": "التفاصيل",
  "tabReviews": "المراجعات",
  "tabRelated": "ذات صلة",

  "navHome": "الرئيسية",
  "navSearch": "بحث",
  "navFavorites": "المفضلة",
  "navProfile": "الملف الشخصي",

  "statUsers": "المستخدمون النشطون",
  "statDownloads": "التنزيلات",
  "statSatisfaction": "الرضا",
  "statRating": "التقييم"
}

Best Practices Summary

Do's

  1. Use FittedBox with scaleDown for text that might overflow
  2. Set alignment to maintain text positioning during scaling
  3. Provide height constraints to control minimum text size
  4. Test with verbose languages like German and Russian
  5. Combine with RTL support using AlignmentDirectional

Don'ts

  1. Don't use FittedBox for body text - it may become unreadable
  2. Don't scale down excessively - ensure minimum readability
  3. Don't forget to test edge cases with longest translations
  4. Don't rely solely on FittedBox - consider layout alternatives

Accessibility Considerations

class AccessibleScalingText extends StatelessWidget {
  final String text;
  final TextStyle? style;

  const AccessibleScalingText({
    super.key,
    required this.text,
    this.style,
  });

  @override
  Widget build(BuildContext context) {
    // Ensure minimum readable font size
    final minFontSize = 12.0;
    final textScaleFactor = MediaQuery.textScaleFactorOf(context);

    return ConstrainedBox(
      constraints: BoxConstraints(
        minHeight: minFontSize * textScaleFactor * 1.5,
      ),
      child: FittedBox(
        fit: BoxFit.scaleDown,
        child: Text(
          text,
          style: style,
        ),
      ),
    );
  }
}

Conclusion

FittedBox is a powerful tool for handling variable text lengths in multilingual Flutter applications. By scaling content to fit available space, it prevents overflow while maintaining readability. Use it strategically for titles, buttons, and labels where text length varies significantly across languages. Always test with your longest translations to ensure content remains readable at minimum scales.

Further Reading