← Back to Blog

Flutter FractionallySizedBox Localization: Proportional Sizing for Global Apps

flutterfractionallysizedboxproportionallayoutlocalizationresponsive

Flutter FractionallySizedBox Localization: Proportional Sizing for Global Apps

FractionallySizedBox sizes its child to a fraction of the total available space. In multilingual applications, this widget enables responsive layouts that adapt gracefully to varying content lengths while maintaining proportional relationships.

Understanding FractionallySizedBox in Localization Context

FractionallySizedBox sizes its child based on a fraction of available space rather than fixed dimensions. For multilingual apps, this creates powerful capabilities:

  • Content areas can grow proportionally with screen size
  • Text containers adapt to different language lengths
  • RTL layouts maintain proper proportional spacing
  • Responsive designs work across all locales

Why FractionallySizedBox Matters for Multilingual Apps

Proportional sizing ensures:

  • Flexible layouts: Containers adapt to available space
  • Consistent proportions: Visual balance across screen sizes
  • Language accommodation: More verbose languages have room to breathe
  • Responsive design: Layouts scale proportionally

Basic FractionallySizedBox Implementation

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

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

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

    return Scaffold(
      body: Center(
        child: FractionallySizedBox(
          widthFactor: 0.8, // 80% of parent width
          child: Card(
            child: Padding(
              padding: const EdgeInsets.all(24),
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  Text(
                    l10n.welcomeTitle,
                    style: Theme.of(context).textTheme.headlineMedium,
                    textAlign: TextAlign.center,
                  ),
                  const SizedBox(height: 16),
                  Text(
                    l10n.welcomeDescription,
                    textAlign: TextAlign.center,
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

Language-Aware Width Factors

Adaptive Width for Text Content

class AdaptiveWidthContainer extends StatelessWidget {
  final Widget child;
  final double baseWidthFactor;

  const AdaptiveWidthContainer({
    super.key,
    required this.child,
    this.baseWidthFactor = 0.8,
  });

  @override
  Widget build(BuildContext context) {
    final locale = Localizations.localeOf(context);
    final adjustedWidth = _getAdjustedWidthFactor(locale);

    return FractionallySizedBox(
      widthFactor: adjustedWidth,
      child: child,
    );
  }

  double _getAdjustedWidthFactor(Locale locale) {
    switch (locale.languageCode) {
      case 'de': // German - typically longer
      case 'ru': // Russian
      case 'fi': // Finnish
      case 'nl': // Dutch
        return (baseWidthFactor + 0.1).clamp(0.0, 1.0);
      case 'ja': // Japanese - more compact
      case 'zh': // Chinese
      case 'ko': // Korean
        return (baseWidthFactor - 0.05).clamp(0.0, 1.0);
      default:
        return baseWidthFactor;
    }
  }
}

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

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

    return AdaptiveWidthContainer(
      baseWidthFactor: 0.75,
      child: Card(
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Text(l10n.longDescription),
        ),
      ),
    );
  }
}

Modal and Dialog Layouts

Localized Dialog with Fractional Width

class LocalizedFractionalDialog extends StatelessWidget {
  final String title;
  final String content;
  final List<Widget> actions;

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

  @override
  Widget build(BuildContext context) {
    return Dialog(
      child: FractionallySizedBox(
        widthFactor: 0.85, // 85% of screen width
        child: Container(
          constraints: const BoxConstraints(maxWidth: 400),
          child: Padding(
            padding: const EdgeInsets.all(24),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  title,
                  style: Theme.of(context).textTheme.headlineSmall,
                ),
                const SizedBox(height: 16),
                Text(
                  content,
                  style: Theme.of(context).textTheme.bodyMedium,
                ),
                const SizedBox(height: 24),
                Row(
                  mainAxisAlignment: MainAxisAlignment.end,
                  children: actions,
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }

  static Future<T?> show<T>(
    BuildContext context, {
    required String title,
    required String content,
    required List<Widget> actions,
  }) {
    return showDialog<T>(
      context: context,
      builder: (context) => LocalizedFractionalDialog(
        title: title,
        content: content,
        actions: actions,
      ),
    );
  }
}

Responsive Bottom Sheet

class LocalizedBottomSheet extends StatelessWidget {
  final String title;
  final Widget content;

  const LocalizedBottomSheet({
    super.key,
    required this.title,
    required this.content,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: Theme.of(context).colorScheme.surface,
        borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
      ),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          // Drag handle
          FractionallySizedBox(
            widthFactor: 0.1,
            child: Container(
              margin: const EdgeInsets.symmetric(vertical: 12),
              height: 4,
              decoration: BoxDecoration(
                color: Theme.of(context).colorScheme.outlineVariant,
                borderRadius: BorderRadius.circular(2),
              ),
            ),
          ),
          // Title
          FractionallySizedBox(
            widthFactor: 0.9,
            child: Padding(
              padding: const EdgeInsets.only(bottom: 16),
              child: Text(
                title,
                style: Theme.of(context).textTheme.titleLarge,
                textAlign: TextAlign.center,
              ),
            ),
          ),
          // Content
          FractionallySizedBox(
            widthFactor: 0.9,
            child: content,
          ),
          const SizedBox(height: 24),
        ],
      ),
    );
  }
}

Form Layouts with Fractional Sizing

Responsive Form Container

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

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

    return Center(
      child: FractionallySizedBox(
        widthFactor: 0.9,
        child: Container(
          constraints: const BoxConstraints(maxWidth: 500),
          child: Card(
            child: Padding(
              padding: const EdgeInsets.all(24),
              child: Column(
                mainAxisSize: MainAxisSize.min,
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  Text(
                    l10n.loginTitle,
                    style: Theme.of(context).textTheme.headlineMedium,
                    textAlign: TextAlign.center,
                  ),
                  const SizedBox(height: 8),
                  Text(
                    l10n.loginSubtitle,
                    style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                      color: Theme.of(context).colorScheme.outline,
                    ),
                    textAlign: TextAlign.center,
                  ),
                  const SizedBox(height: 32),
                  _LocalizedTextField(
                    label: l10n.emailLabel,
                    hint: l10n.emailHint,
                    keyboardType: TextInputType.emailAddress,
                  ),
                  const SizedBox(height: 16),
                  _LocalizedTextField(
                    label: l10n.passwordLabel,
                    hint: l10n.passwordHint,
                    obscureText: true,
                  ),
                  const SizedBox(height: 24),
                  FilledButton(
                    onPressed: () {},
                    child: Padding(
                      padding: const EdgeInsets.symmetric(vertical: 12),
                      child: Text(l10n.loginButton),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class _LocalizedTextField extends StatelessWidget {
  final String label;
  final String hint;
  final TextInputType? keyboardType;
  final bool obscureText;

  const _LocalizedTextField({
    required this.label,
    required this.hint,
    this.keyboardType,
    this.obscureText = false,
  });

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Padding(
          padding: const EdgeInsetsDirectional.only(start: 4, bottom: 8),
          child: Text(
            label,
            style: Theme.of(context).textTheme.labelLarge,
          ),
        ),
        TextField(
          keyboardType: keyboardType,
          obscureText: obscureText,
          decoration: InputDecoration(
            hintText: hint,
            border: const OutlineInputBorder(),
          ),
        ),
      ],
    );
  }
}

Progress and Loading Indicators

Localized Progress Bar

class LocalizedProgressBar extends StatelessWidget {
  final double progress;
  final String label;

  const LocalizedProgressBar({
    super.key,
    required this.progress,
    required this.label,
  });

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text(
              label,
              style: Theme.of(context).textTheme.bodyMedium,
            ),
            Text(
              '${(progress * 100).toInt()}%',
              style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                fontWeight: FontWeight.bold,
              ),
            ),
          ],
        ),
        const SizedBox(height: 8),
        Container(
          height: 8,
          decoration: BoxDecoration(
            color: Theme.of(context).colorScheme.surfaceContainerHighest,
            borderRadius: BorderRadius.circular(4),
          ),
          child: Align(
            alignment: AlignmentDirectional.centerStart,
            child: FractionallySizedBox(
              widthFactor: progress.clamp(0.0, 1.0),
              child: Container(
                decoration: BoxDecoration(
                  color: Theme.of(context).colorScheme.primary,
                  borderRadius: BorderRadius.circular(4),
                ),
              ),
            ),
          ),
        ),
      ],
    );
  }
}

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

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

    return Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        children: [
          LocalizedProgressBar(
            progress: 0.65,
            label: l10n.downloadProgress,
          ),
          const SizedBox(height: 24),
          LocalizedProgressBar(
            progress: 0.30,
            label: l10n.uploadProgress,
          ),
        ],
      ),
    );
  }
}

Split View Layouts

Localized Split Panel

class LocalizedSplitPanel extends StatelessWidget {
  final Widget leftPanel;
  final Widget rightPanel;
  final double leftFactor;

  const LocalizedSplitPanel({
    super.key,
    required this.leftPanel,
    required this.rightPanel,
    this.leftFactor = 0.3,
  });

  @override
  Widget build(BuildContext context) {
    final isRtl = Directionality.of(context) == TextDirection.rtl;

    return Row(
      children: [
        FractionallySizedBox(
          widthFactor: leftFactor,
          child: isRtl ? rightPanel : leftPanel,
        ),
        Expanded(
          child: isRtl ? leftPanel : rightPanel,
        ),
      ],
    );
  }
}

// Adaptive split panel that adjusts to language
class AdaptiveSplitPanel extends StatelessWidget {
  final Widget navigationPanel;
  final Widget contentPanel;

  const AdaptiveSplitPanel({
    super.key,
    required this.navigationPanel,
    required this.contentPanel,
  });

  @override
  Widget build(BuildContext context) {
    final locale = Localizations.localeOf(context);
    final navWidth = _getNavigationWidth(locale);

    return Row(
      children: [
        SizedBox(
          width: navWidth,
          child: navigationPanel,
        ),
        Expanded(child: contentPanel),
      ],
    );
  }

  double _getNavigationWidth(Locale locale) {
    // Adjust navigation width for verbose languages
    switch (locale.languageCode) {
      case 'de':
      case 'ru':
      case 'fi':
        return 280; // Wider for longer menu items
      case 'ja':
      case 'zh':
        return 200; // Narrower for compact text
      default:
        return 240;
    }
  }
}

Onboarding Screens

Localized Onboarding Page

class LocalizedOnboardingPage extends StatelessWidget {
  final String imageAsset;
  final String title;
  final String description;

  const LocalizedOnboardingPage({
    super.key,
    required this.imageAsset,
    required this.title,
    required this.description,
  });

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(24),
      child: Column(
        children: [
          Expanded(
            flex: 3,
            child: FractionallySizedBox(
              widthFactor: 0.8,
              child: Image.asset(
                imageAsset,
                fit: BoxFit.contain,
              ),
            ),
          ),
          Expanded(
            flex: 2,
            child: FractionallySizedBox(
              widthFactor: 0.9,
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text(
                    title,
                    style: Theme.of(context).textTheme.headlineMedium?.copyWith(
                      fontWeight: FontWeight.bold,
                    ),
                    textAlign: TextAlign.center,
                  ),
                  const SizedBox(height: 16),
                  Text(
                    description,
                    style: Theme.of(context).textTheme.bodyLarge?.copyWith(
                      color: Theme.of(context).colorScheme.outline,
                    ),
                    textAlign: TextAlign.center,
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

ARB File Structure

English (app_en.arb)

{
  "@@locale": "en",

  "welcomeTitle": "Welcome",
  "@welcomeTitle": {
    "description": "Welcome screen title"
  },

  "welcomeDescription": "Discover amazing features and start your journey with our app today.",
  "@welcomeDescription": {
    "description": "Welcome screen description"
  },

  "longDescription": "This comprehensive guide will help you understand all the features and capabilities available to you as a user of our application.",
  "@longDescription": {
    "description": "Long content description"
  },

  "loginTitle": "Welcome Back",
  "loginSubtitle": "Sign in to continue",
  "emailLabel": "Email",
  "emailHint": "Enter your email",
  "passwordLabel": "Password",
  "passwordHint": "Enter your password",
  "loginButton": "Sign In",

  "downloadProgress": "Downloading...",
  "uploadProgress": "Uploading..."
}

German (app_de.arb)

{
  "@@locale": "de",

  "welcomeTitle": "Willkommen",
  "welcomeDescription": "Entdecken Sie erstaunliche Funktionen und beginnen Sie noch heute Ihre Reise mit unserer App.",

  "longDescription": "Dieser umfassende Leitfaden hilft Ihnen, alle Funktionen und Möglichkeiten zu verstehen, die Ihnen als Benutzer unserer Anwendung zur Verfügung stehen.",

  "loginTitle": "Willkommen zurück",
  "loginSubtitle": "Melden Sie sich an, um fortzufahren",
  "emailLabel": "E-Mail-Adresse",
  "emailHint": "Geben Sie Ihre E-Mail-Adresse ein",
  "passwordLabel": "Passwort",
  "passwordHint": "Geben Sie Ihr Passwort ein",
  "loginButton": "Anmelden",

  "downloadProgress": "Wird heruntergeladen...",
  "uploadProgress": "Wird hochgeladen..."
}

Arabic (app_ar.arb)

{
  "@@locale": "ar",

  "welcomeTitle": "مرحباً",
  "welcomeDescription": "اكتشف ميزات مذهلة وابدأ رحلتك مع تطبيقنا اليوم.",

  "longDescription": "سيساعدك هذا الدليل الشامل على فهم جميع الميزات والإمكانيات المتاحة لك كمستخدم لتطبيقنا.",

  "loginTitle": "مرحباً بعودتك",
  "loginSubtitle": "سجّل الدخول للمتابعة",
  "emailLabel": "البريد الإلكتروني",
  "emailHint": "أدخل بريدك الإلكتروني",
  "passwordLabel": "كلمة المرور",
  "passwordHint": "أدخل كلمة المرور",
  "loginButton": "تسجيل الدخول",

  "downloadProgress": "جارٍ التنزيل...",
  "uploadProgress": "جارٍ الرفع..."
}

Best Practices Summary

Do's

  1. Use FractionallySizedBox for responsive layouts that need proportional sizing
  2. Combine with constraints (maxWidth, maxHeight) for reasonable limits
  3. Adjust width factors for languages with different text lengths
  4. Test progress indicators in RTL mode for proper direction
  5. Use for modal widths to ensure consistent presentation

Don'ts

  1. Don't use without constraints when content might overflow
  2. Don't ignore text overflow possibilities
  3. Don't assume fixed content across all languages
  4. Don't forget RTL testing for progress bars and split views

Accessibility Considerations

class AccessibleFractionalContent extends StatelessWidget {
  final Widget child;
  final String semanticLabel;

  const AccessibleFractionalContent({
    super.key,
    required this.child,
    required this.semanticLabel,
  });

  @override
  Widget build(BuildContext context) {
    return Semantics(
      label: semanticLabel,
      child: FractionallySizedBox(
        widthFactor: 0.9,
        child: child,
      ),
    );
  }
}

Conclusion

FractionallySizedBox is invaluable for creating responsive, proportionally-sized layouts in multilingual Flutter applications. By adapting width factors based on language characteristics and combining with proper constraints, you can build flexible interfaces that gracefully accommodate content variations across all supported locales.

Further Reading