← Back to Blog

Flutter Align Localization: Precision Positioning for Multilingual Apps

flutteralignpositioningbadgeslocalizationrtl

Flutter Align Localization: Precision Positioning for Multilingual Apps

The Align widget provides precise control over child positioning within available space. In multilingual applications, Align becomes essential for creating layouts that adapt gracefully to different languages, text directions, and content sizes while maintaining visual harmony.

Understanding Align in Localization Context

Align positions its child according to an Alignment value, which can range from -1.0 (start) to 1.0 (end) on both axes. For multilingual apps, this matters because:

  • RTL languages flip the meaning of left/right alignments
  • Text-heavy UI elements need directional alignment
  • Different language lengths require adaptive positioning
  • Visual balance varies across writing systems

Why Align Matters for Multilingual Apps

Proper alignment ensures:

  • Badges and indicators: Position correctly in RTL and LTR layouts
  • Floating elements: Adapt to text direction changes
  • Overlay content: Stay contextually positioned
  • Card decorations: Maintain visual consistency across languages

Basic Align Implementation

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

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

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

    return Stack(
      children: [
        Card(
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  l10n.cardTitle,
                  style: Theme.of(context).textTheme.titleMedium,
                ),
                const SizedBox(height: 8),
                Text(l10n.cardDescription),
              ],
            ),
          ),
        ),
        Align(
          alignment: AlignmentDirectional.topEnd,
          child: Container(
            margin: const EdgeInsets.all(8),
            padding: const EdgeInsets.symmetric(
              horizontal: 8,
              vertical: 4,
            ),
            decoration: BoxDecoration(
              color: Theme.of(context).colorScheme.primary,
              borderRadius: BorderRadius.circular(12),
            ),
            child: Text(
              l10n.newBadge,
              style: TextStyle(
                color: Theme.of(context).colorScheme.onPrimary,
                fontSize: 12,
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
        ),
      ],
    );
  }
}

Directional Alignment Patterns

RTL-Aware Alignment Widget

class DirectionalAlign extends StatelessWidget {
  final Widget child;
  final AlignmentDirectional alignment;
  final double? widthFactor;
  final double? heightFactor;

  const DirectionalAlign({
    super.key,
    required this.child,
    required this.alignment,
    this.widthFactor,
    this.heightFactor,
  });

  @override
  Widget build(BuildContext context) {
    return Align(
      alignment: alignment,
      widthFactor: widthFactor,
      heightFactor: heightFactor,
      child: child,
    );
  }
}

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

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

    return Column(
      children: [
        // Start-aligned text (left in LTR, right in RTL)
        DirectionalAlign(
          alignment: AlignmentDirectional.centerStart,
          child: Text(l10n.startAlignedText),
        ),
        const SizedBox(height: 16),

        // End-aligned text (right in LTR, left in RTL)
        DirectionalAlign(
          alignment: AlignmentDirectional.centerEnd,
          child: Text(l10n.endAlignedText),
        ),
        const SizedBox(height: 16),

        // Top-start corner
        DirectionalAlign(
          alignment: AlignmentDirectional.topStart,
          child: Icon(Icons.info),
        ),
      ],
    );
  }
}

Badge Positioning with Align

Localized Badge Component

class LocalizedBadgeContainer extends StatelessWidget {
  final Widget child;
  final String? badgeText;
  final int? badgeCount;
  final bool showBadge;

  const LocalizedBadgeContainer({
    super.key,
    required this.child,
    this.badgeText,
    this.badgeCount,
    this.showBadge = true,
  });

  @override
  Widget build(BuildContext context) {
    if (!showBadge || (badgeText == null && badgeCount == null)) {
      return child;
    }

    return Stack(
      clipBehavior: Clip.none,
      children: [
        child,
        Positioned(
          top: -4,
          right: Directionality.of(context) == TextDirection.ltr ? -4 : null,
          left: Directionality.of(context) == TextDirection.rtl ? -4 : null,
          child: _Badge(
            text: badgeText,
            count: badgeCount,
          ),
        ),
      ],
    );
  }
}

class _Badge extends StatelessWidget {
  final String? text;
  final int? count;

  const _Badge({this.text, this.count});

  @override
  Widget build(BuildContext context) {
    final displayText = text ?? (count != null ? '$count' : '');

    return Container(
      constraints: const BoxConstraints(
        minWidth: 20,
        minHeight: 20,
      ),
      padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
      decoration: BoxDecoration(
        color: Theme.of(context).colorScheme.error,
        borderRadius: BorderRadius.circular(10),
      ),
      child: Center(
        child: Text(
          displayText,
          style: TextStyle(
            color: Theme.of(context).colorScheme.onError,
            fontSize: 12,
            fontWeight: FontWeight.bold,
          ),
        ),
      ),
    );
  }
}

// Usage
class NotificationIcon extends StatelessWidget {
  final int unreadCount;

  const NotificationIcon({
    super.key,
    required this.unreadCount,
  });

  @override
  Widget build(BuildContext context) {
    return LocalizedBadgeContainer(
      badgeCount: unreadCount > 99 ? 99 : unreadCount,
      showBadge: unreadCount > 0,
      child: IconButton(
        icon: const Icon(Icons.notifications_outlined),
        onPressed: () {},
      ),
    );
  }
}

Floating Action Positioning

Localized FAB Placement

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

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

    return Stack(
      children: [
        // Main content
        ListView.builder(
          itemCount: 20,
          itemBuilder: (context, index) => ListTile(
            title: Text('${l10n.itemLabel} ${index + 1}'),
          ),
        ),

        // Primary FAB - bottom end (respects RTL)
        Align(
          alignment: AlignmentDirectional.bottomEnd,
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: FloatingActionButton.extended(
              heroTag: 'primary',
              onPressed: () {},
              icon: const Icon(Icons.add),
              label: Text(l10n.addItemButton),
            ),
          ),
        ),

        // Secondary FAB - above primary
        Align(
          alignment: AlignmentDirectional.bottomEnd,
          child: Padding(
            padding: const EdgeInsets.only(
              bottom: 88,
              right: 16,
              left: 16,
            ),
            child: FloatingActionButton.small(
              heroTag: 'secondary',
              onPressed: () {},
              child: const Icon(Icons.filter_list),
            ),
          ),
        ),
      ],
    );
  }
}

Overlay Alignment

Localized Tooltip Positioning

class LocalizedTooltipOverlay extends StatelessWidget {
  final Widget child;
  final String tooltipText;
  final bool showTooltip;

  const LocalizedTooltipOverlay({
    super.key,
    required this.child,
    required this.tooltipText,
    this.showTooltip = false,
  });

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

    return Stack(
      clipBehavior: Clip.none,
      children: [
        child,
        if (showTooltip)
          Positioned(
            bottom: -40,
            left: isRtl ? null : 0,
            right: isRtl ? 0 : null,
            child: Container(
              padding: const EdgeInsets.symmetric(
                horizontal: 12,
                vertical: 8,
              ),
              decoration: BoxDecoration(
                color: Theme.of(context).colorScheme.inverseSurface,
                borderRadius: BorderRadius.circular(8),
              ),
              child: Row(
                mainAxisSize: MainAxisSize.min,
                textDirection: isRtl ? TextDirection.rtl : TextDirection.ltr,
                children: [
                  Icon(
                    Icons.info_outline,
                    size: 16,
                    color: Theme.of(context).colorScheme.onInverseSurface,
                  ),
                  const SizedBox(width: 8),
                  Text(
                    tooltipText,
                    style: TextStyle(
                      color: Theme.of(context).colorScheme.onInverseSurface,
                      fontSize: 12,
                    ),
                  ),
                ],
              ),
            ),
          ),
      ],
    );
  }
}

Card Corner Decorations

Localized Ribbon Component

class LocalizedRibbonCard extends StatelessWidget {
  final String ribbonText;
  final Widget child;
  final Color? ribbonColor;

  const LocalizedRibbonCard({
    super.key,
    required this.ribbonText,
    required this.child,
    this.ribbonColor,
  });

  @override
  Widget build(BuildContext context) {
    final isRtl = Directionality.of(context) == TextDirection.rtl;
    final color = ribbonColor ?? Theme.of(context).colorScheme.primary;

    return Stack(
      children: [
        child,
        Align(
          alignment: isRtl
              ? Alignment.topLeft
              : Alignment.topRight,
          child: ClipRect(
            child: Transform.rotate(
              angle: isRtl ? -0.785 : 0.785, // 45 degrees
              child: Transform.translate(
                offset: Offset(isRtl ? -30 : 30, -10),
                child: Container(
                  width: 120,
                  padding: const EdgeInsets.symmetric(vertical: 4),
                  color: color,
                  child: Text(
                    ribbonText,
                    textAlign: TextAlign.center,
                    style: TextStyle(
                      color: Theme.of(context).colorScheme.onPrimary,
                      fontSize: 12,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
              ),
            ),
          ),
        ),
      ],
    );
  }
}

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

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

    return LocalizedRibbonCard(
      ribbonText: l10n.saleRibbon,
      child: Card(
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                l10n.productName,
                style: Theme.of(context).textTheme.titleMedium,
              ),
              const SizedBox(height: 8),
              Text(l10n.productPrice),
            ],
          ),
        ),
      ),
    );
  }
}

Status Indicator Alignment

Localized Status Badge

class LocalizedStatusIndicator extends StatelessWidget {
  final Widget child;
  final String status;
  final Color statusColor;

  const LocalizedStatusIndicator({
    super.key,
    required this.child,
    required this.status,
    required this.statusColor,
  });

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        child,
        Align(
          alignment: AlignmentDirectional.bottomEnd,
          child: Transform.translate(
            offset: const Offset(4, 4),
            child: Container(
              padding: const EdgeInsets.symmetric(
                horizontal: 8,
                vertical: 4,
              ),
              decoration: BoxDecoration(
                color: statusColor,
                borderRadius: BorderRadius.circular(12),
                border: Border.all(
                  color: Theme.of(context).colorScheme.surface,
                  width: 2,
                ),
              ),
              child: Text(
                status,
                style: const TextStyle(
                  color: Colors.white,
                  fontSize: 10,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ),
          ),
        ),
      ],
    );
  }
}

// Usage
class UserAvatar extends StatelessWidget {
  final String imageUrl;
  final bool isOnline;

  const UserAvatar({
    super.key,
    required this.imageUrl,
    required this.isOnline,
  });

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

    return LocalizedStatusIndicator(
      status: isOnline ? l10n.statusOnline : l10n.statusOffline,
      statusColor: isOnline ? Colors.green : Colors.grey,
      child: CircleAvatar(
        radius: 30,
        backgroundImage: NetworkImage(imageUrl),
      ),
    );
  }
}

Align in Form Layouts

Aligned Form Actions

class LocalizedFormActions extends StatelessWidget {
  final VoidCallback onSubmit;
  final VoidCallback onCancel;

  const LocalizedFormActions({
    super.key,
    required this.onSubmit,
    required this.onCancel,
  });

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

    return Align(
      alignment: AlignmentDirectional.centerEnd,
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          TextButton(
            onPressed: onCancel,
            child: Text(l10n.cancelButton),
          ),
          const SizedBox(width: 12),
          ElevatedButton(
            onPressed: onSubmit,
            child: Text(l10n.submitButton),
          ),
        ],
      ),
    );
  }
}

Progress Indicator Alignment

Localized Progress Overlay

class LocalizedProgressOverlay extends StatelessWidget {
  final Widget child;
  final bool isLoading;
  final String? loadingText;
  final AlignmentDirectional alignment;

  const LocalizedProgressOverlay({
    super.key,
    required this.child,
    required this.isLoading,
    this.loadingText,
    this.alignment = AlignmentDirectional.center,
  });

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

    return Stack(
      children: [
        child,
        if (isLoading)
          Positioned.fill(
            child: Container(
              color: Colors.black26,
              child: Align(
                alignment: alignment,
                child: Card(
                  child: Padding(
                    padding: const EdgeInsets.all(24),
                    child: Column(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        const CircularProgressIndicator(),
                        const SizedBox(height: 16),
                        Text(loadingText ?? l10n.pleaseWait),
                      ],
                    ),
                  ),
                ),
              ),
            ),
          ),
      ],
    );
  }
}

Alignment with Animation

Animated Alignment for Localized Content

class AnimatedLocalizedAlign extends StatefulWidget {
  final bool isExpanded;

  const AnimatedLocalizedAlign({
    super.key,
    required this.isExpanded,
  });

  @override
  State<AnimatedLocalizedAlign> createState() => _AnimatedLocalizedAlignState();
}

class _AnimatedLocalizedAlignState extends State<AnimatedLocalizedAlign> {
  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;
    final isRtl = Directionality.of(context) == TextDirection.rtl;

    return AnimatedAlign(
      alignment: widget.isExpanded
          ? Alignment.center
          : (isRtl ? Alignment.centerRight : Alignment.centerLeft),
      duration: const Duration(milliseconds: 300),
      curve: Curves.easeInOut,
      child: Container(
        padding: const EdgeInsets.all(16),
        decoration: BoxDecoration(
          color: Theme.of(context).colorScheme.primaryContainer,
          borderRadius: BorderRadius.circular(12),
        ),
        child: Text(
          widget.isExpanded ? l10n.expandedState : l10n.collapsedState,
          style: Theme.of(context).textTheme.titleMedium,
        ),
      ),
    );
  }
}

ARB File Structure

English (app_en.arb)

{
  "@@locale": "en",

  "cardTitle": "Featured Item",
  "@cardTitle": {
    "description": "Title for featured card"
  },

  "cardDescription": "This is a special item with exclusive features.",
  "@cardDescription": {
    "description": "Description for featured card"
  },

  "newBadge": "NEW",
  "@newBadge": {
    "description": "Badge text for new items"
  },

  "startAlignedText": "This text aligns to the start",
  "endAlignedText": "This text aligns to the end",

  "itemLabel": "Item",
  "addItemButton": "Add Item",

  "saleRibbon": "SALE",
  "@saleRibbon": {
    "description": "Ribbon text for sale items"
  },

  "productName": "Premium Product",
  "productPrice": "$99.99",

  "statusOnline": "Online",
  "@statusOnline": {
    "description": "Online status indicator"
  },

  "statusOffline": "Offline",
  "@statusOffline": {
    "description": "Offline status indicator"
  },

  "cancelButton": "Cancel",
  "submitButton": "Submit",

  "pleaseWait": "Please wait...",
  "@pleaseWait": {
    "description": "Generic loading message"
  },

  "expandedState": "Expanded View",
  "collapsedState": "Collapsed",

  "alignTopStart": "Top Start",
  "alignTopCenter": "Top Center",
  "alignTopEnd": "Top End",
  "alignCenterStart": "Center Start",
  "alignCenter": "Center",
  "alignCenterEnd": "Center End",
  "alignBottomStart": "Bottom Start",
  "alignBottomCenter": "Bottom Center",
  "alignBottomEnd": "Bottom End"
}

German (app_de.arb)

{
  "@@locale": "de",

  "cardTitle": "Hervorgehobener Artikel",
  "cardDescription": "Dies ist ein besonderer Artikel mit exklusiven Funktionen.",

  "newBadge": "NEU",

  "startAlignedText": "Dieser Text ist am Anfang ausgerichtet",
  "endAlignedText": "Dieser Text ist am Ende ausgerichtet",

  "itemLabel": "Artikel",
  "addItemButton": "Artikel hinzufügen",

  "saleRibbon": "SALE",

  "productName": "Premium-Produkt",
  "productPrice": "99,99 €",

  "statusOnline": "Online",
  "statusOffline": "Offline",

  "cancelButton": "Abbrechen",
  "submitButton": "Absenden",

  "pleaseWait": "Bitte warten...",

  "expandedState": "Erweiterte Ansicht",
  "collapsedState": "Eingeklappt",

  "alignTopStart": "Oben Anfang",
  "alignTopCenter": "Oben Mitte",
  "alignTopEnd": "Oben Ende",
  "alignCenterStart": "Mitte Anfang",
  "alignCenter": "Mitte",
  "alignCenterEnd": "Mitte Ende",
  "alignBottomStart": "Unten Anfang",
  "alignBottomCenter": "Unten Mitte",
  "alignBottomEnd": "Unten Ende"
}

Arabic (app_ar.arb)

{
  "@@locale": "ar",

  "cardTitle": "عنصر مميز",
  "cardDescription": "هذا عنصر خاص بميزات حصرية.",

  "newBadge": "جديد",

  "startAlignedText": "هذا النص محاذاة للبداية",
  "endAlignedText": "هذا النص محاذاة للنهاية",

  "itemLabel": "عنصر",
  "addItemButton": "إضافة عنصر",

  "saleRibbon": "تخفيض",

  "productName": "منتج متميز",
  "productPrice": "٩٩٫٩٩ دولار",

  "statusOnline": "متصل",
  "statusOffline": "غير متصل",

  "cancelButton": "إلغاء",
  "submitButton": "إرسال",

  "pleaseWait": "يرجى الانتظار...",

  "expandedState": "عرض موسع",
  "collapsedState": "مطوي",

  "alignTopStart": "أعلى البداية",
  "alignTopCenter": "أعلى الوسط",
  "alignTopEnd": "أعلى النهاية",
  "alignCenterStart": "وسط البداية",
  "alignCenter": "الوسط",
  "alignCenterEnd": "وسط النهاية",
  "alignBottomStart": "أسفل البداية",
  "alignBottomCenter": "أسفل الوسط",
  "alignBottomEnd": "أسفل النهاية"
}

Best Practices Summary

Do's

  1. Use AlignmentDirectional instead of Alignment for RTL support
  2. Test badge positions in both LTR and RTL layouts
  3. Combine with Positioned in Stack for complex overlays
  4. Use widthFactor/heightFactor to size Align relative to child
  5. Consider visual weight when positioning elements across languages

Don'ts

  1. Don't use Alignment.centerLeft/Right in RTL-supporting apps
  2. Don't hardcode positions that should flip in RTL
  3. Don't ignore text overflow when aligning variable-length content
  4. Don't assume badge positions work the same in all languages

Accessibility Considerations

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

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

    return Semantics(
      container: true,
      child: Stack(
        children: [
          Card(
            child: Semantics(
              label: l10n.productCardLabel,
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Text(l10n.productName),
              ),
            ),
          ),
          Align(
            alignment: AlignmentDirectional.topEnd,
            child: Semantics(
              label: l10n.saleBadgeLabel,
              child: Container(
                padding: const EdgeInsets.all(8),
                color: Colors.red,
                child: Text(l10n.saleRibbon),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

Conclusion

The Align widget is a powerful tool for precise positioning in Flutter layouts. By using AlignmentDirectional and understanding how different languages affect visual layout, you can create polished, adaptive interfaces that work seamlessly across all locales. Always test your aligned elements with actual translations and in RTL mode to ensure they position correctly in all scenarios.

Further Reading