← Back to Blog

Flutter AnimatedAlign Localization: Dynamic Positioning, RTL Alignment, and Accessible Transitions

flutteranimatedalignanimationalignmentlocalizationrtl

Flutter AnimatedAlign Localization: Dynamic Positioning, RTL Alignment, and Accessible Transitions

AnimatedAlign smoothly animates changes to widget alignment within its parent. Proper localization ensures that alignment transitions, RTL-aware positioning, and dynamic layouts work seamlessly across languages. This guide covers comprehensive strategies for localizing AnimatedAlign widgets in Flutter.

Understanding AnimatedAlign Localization

AnimatedAlign widgets require localization for:

  • RTL alignment: Mirroring alignments for right-to-left languages
  • Dynamic positioning: Moving content based on localized states
  • Focus indicators: Positioning highlights based on text direction
  • Interactive elements: Aligning feedback elements correctly
  • Accessibility: Announcing position changes for screen readers
  • Responsive layouts: Adapting alignment to content direction

Basic AnimatedAlign with RTL Support

Start with a simple AnimatedAlign that respects text direction:

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

class LocalizedAlignedContent extends StatefulWidget {
  const LocalizedAlignedContent({super.key});

  @override
  State<LocalizedAlignedContent> createState() => _LocalizedAlignedContentState();
}

class _LocalizedAlignedContentState extends State<LocalizedAlignedContent> {
  bool _isExpanded = false;

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

    // Determine alignment based on state and text direction
    final alignment = _isExpanded
        ? Alignment.center
        : (isRtl ? Alignment.centerRight : Alignment.centerLeft);

    return Scaffold(
      appBar: AppBar(title: Text(l10n.alignmentDemoTitle)),
      body: Column(
        children: [
          Expanded(
            child: Container(
              margin: const EdgeInsets.all(16),
              decoration: BoxDecoration(
                border: Border.all(
                  color: Theme.of(context).colorScheme.outline,
                ),
                borderRadius: BorderRadius.circular(12),
              ),
              child: AnimatedAlign(
                duration: const Duration(milliseconds: 400),
                curve: Curves.easeOutCubic,
                alignment: alignment,
                child: Semantics(
                  label: _isExpanded
                      ? l10n.contentCenteredAccessibility
                      : l10n.contentStartAccessibility,
                  child: Card(
                    elevation: 4,
                    child: Padding(
                      padding: const EdgeInsets.all(24),
                      child: Column(
                        mainAxisSize: MainAxisSize.min,
                        children: [
                          Icon(
                            _isExpanded ? Icons.center_focus_strong : Icons.format_align_left,
                            size: 48,
                            color: Theme.of(context).colorScheme.primary,
                          ),
                          const SizedBox(height: 16),
                          Text(
                            _isExpanded ? l10n.centeredContent : l10n.alignedContent,
                            style: Theme.of(context).textTheme.titleMedium,
                          ),
                        ],
                      ),
                    ),
                  ),
                ),
              ),
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(16),
            child: ElevatedButton.icon(
              onPressed: () => setState(() => _isExpanded = !_isExpanded),
              icon: Icon(_isExpanded ? Icons.compress : Icons.expand),
              label: Text(_isExpanded ? l10n.alignToStart : l10n.centerContent),
            ),
          ),
        ],
      ),
    );
  }
}

ARB File Structure for AnimatedAlign

{
  "alignmentDemoTitle": "Alignment Demo",
  "@alignmentDemoTitle": {
    "description": "Title for alignment demo screen"
  },
  "contentCenteredAccessibility": "Content is centered",
  "@contentCenteredAccessibility": {
    "description": "Accessibility label when content is centered"
  },
  "contentStartAccessibility": "Content is aligned to start",
  "@contentStartAccessibility": {
    "description": "Accessibility label when content is at start"
  },
  "centeredContent": "Centered",
  "@centeredContent": {
    "description": "Label for centered state"
  },
  "alignedContent": "Start Aligned",
  "@alignedContent": {
    "description": "Label for start-aligned state"
  },
  "centerContent": "Center Content",
  "@centerContent": {
    "description": "Button label to center content"
  },
  "alignToStart": "Align to Start",
  "@alignToStart": {
    "description": "Button label to align content to start"
  }
}

Multi-Position Alignment Selector

Create a selector that moves content to different positions:

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

enum ContentPosition { topStart, topCenter, topEnd, centerStart, center, centerEnd, bottomStart, bottomCenter, bottomEnd }

class LocalizedPositionSelector extends StatefulWidget {
  const LocalizedPositionSelector({super.key});

  @override
  State<LocalizedPositionSelector> createState() => _LocalizedPositionSelectorState();
}

class _LocalizedPositionSelectorState extends State<LocalizedPositionSelector> {
  ContentPosition _position = ContentPosition.center;

  Alignment _getAlignment(ContentPosition position, bool isRtl) {
    // Map positions to alignments, respecting RTL
    final alignments = {
      ContentPosition.topStart: isRtl ? Alignment.topRight : Alignment.topLeft,
      ContentPosition.topCenter: Alignment.topCenter,
      ContentPosition.topEnd: isRtl ? Alignment.topLeft : Alignment.topRight,
      ContentPosition.centerStart: isRtl ? Alignment.centerRight : Alignment.centerLeft,
      ContentPosition.center: Alignment.center,
      ContentPosition.centerEnd: isRtl ? Alignment.centerLeft : Alignment.centerRight,
      ContentPosition.bottomStart: isRtl ? Alignment.bottomRight : Alignment.bottomLeft,
      ContentPosition.bottomCenter: Alignment.bottomCenter,
      ContentPosition.bottomEnd: isRtl ? Alignment.bottomLeft : Alignment.bottomRight,
    };
    return alignments[position]!;
  }

  String _getPositionLabel(AppLocalizations l10n, ContentPosition position) {
    return switch (position) {
      ContentPosition.topStart => l10n.positionTopStart,
      ContentPosition.topCenter => l10n.positionTopCenter,
      ContentPosition.topEnd => l10n.positionTopEnd,
      ContentPosition.centerStart => l10n.positionCenterStart,
      ContentPosition.center => l10n.positionCenter,
      ContentPosition.centerEnd => l10n.positionCenterEnd,
      ContentPosition.bottomStart => l10n.positionBottomStart,
      ContentPosition.bottomCenter => l10n.positionBottomCenter,
      ContentPosition.bottomEnd => l10n.positionBottomEnd,
    };
  }

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

    return Scaffold(
      appBar: AppBar(title: Text(l10n.positionSelectorTitle)),
      body: Column(
        children: [
          // Preview area
          Expanded(
            child: Container(
              margin: const EdgeInsets.all(16),
              decoration: BoxDecoration(
                color: Theme.of(context).colorScheme.surfaceVariant,
                borderRadius: BorderRadius.circular(16),
              ),
              child: AnimatedAlign(
                duration: const Duration(milliseconds: 500),
                curve: Curves.easeInOutCubic,
                alignment: _getAlignment(_position, isRtl),
                child: Semantics(
                  liveRegion: true,
                  label: l10n.contentAtPosition(_getPositionLabel(l10n, _position)),
                  child: Container(
                    padding: const EdgeInsets.all(20),
                    decoration: BoxDecoration(
                      color: Theme.of(context).colorScheme.primary,
                      borderRadius: BorderRadius.circular(12),
                      boxShadow: [
                        BoxShadow(
                          color: Theme.of(context).colorScheme.primary.withOpacity(0.3),
                          blurRadius: 12,
                          offset: const Offset(0, 4),
                        ),
                      ],
                    ),
                    child: Icon(
                      Icons.widgets,
                      color: Theme.of(context).colorScheme.onPrimary,
                      size: 32,
                    ),
                  ),
                ),
              ),
            ),
          ),
          // Position grid
          Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  l10n.selectPosition,
                  style: Theme.of(context).textTheme.titleMedium,
                ),
                const SizedBox(height: 12),
                _buildPositionGrid(l10n, isRtl),
              ],
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildPositionGrid(AppLocalizations l10n, bool isRtl) {
    final positions = [
      [ContentPosition.topStart, ContentPosition.topCenter, ContentPosition.topEnd],
      [ContentPosition.centerStart, ContentPosition.center, ContentPosition.centerEnd],
      [ContentPosition.bottomStart, ContentPosition.bottomCenter, ContentPosition.bottomEnd],
    ];

    return Column(
      children: positions.map((row) {
        return Row(
          children: row.map((position) {
            final isSelected = _position == position;
            return Expanded(
              child: Padding(
                padding: const EdgeInsets.all(4),
                child: Semantics(
                  selected: isSelected,
                  button: true,
                  label: l10n.selectPositionAccessibility(_getPositionLabel(l10n, position)),
                  child: InkWell(
                    onTap: () => setState(() => _position = position),
                    borderRadius: BorderRadius.circular(8),
                    child: AnimatedContainer(
                      duration: const Duration(milliseconds: 200),
                      padding: const EdgeInsets.symmetric(vertical: 12),
                      decoration: BoxDecoration(
                        color: isSelected
                            ? Theme.of(context).colorScheme.primary
                            : Theme.of(context).colorScheme.surface,
                        borderRadius: BorderRadius.circular(8),
                        border: Border.all(
                          color: isSelected
                              ? Theme.of(context).colorScheme.primary
                              : Theme.of(context).colorScheme.outline,
                        ),
                      ),
                      child: Icon(
                        _getPositionIcon(position),
                        color: isSelected
                            ? Theme.of(context).colorScheme.onPrimary
                            : Theme.of(context).colorScheme.onSurface,
                        size: 20,
                      ),
                    ),
                  ),
                ),
              ),
            );
          }).toList(),
        );
      }).toList(),
    );
  }

  IconData _getPositionIcon(ContentPosition position) {
    return switch (position) {
      ContentPosition.topStart => Icons.north_west,
      ContentPosition.topCenter => Icons.north,
      ContentPosition.topEnd => Icons.north_east,
      ContentPosition.centerStart => Icons.west,
      ContentPosition.center => Icons.center_focus_strong,
      ContentPosition.centerEnd => Icons.east,
      ContentPosition.bottomStart => Icons.south_west,
      ContentPosition.bottomCenter => Icons.south,
      ContentPosition.bottomEnd => Icons.south_east,
    };
  }
}

Chat Message Alignment

Create chat bubbles with proper alignment based on sender:

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

class LocalizedChatMessage extends StatelessWidget {
  final String message;
  final String sender;
  final bool isCurrentUser;
  final DateTime timestamp;

  const LocalizedChatMessage({
    super.key,
    required this.message,
    required this.sender,
    required this.isCurrentUser,
    required this.timestamp,
  });

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

    // Determine alignment: current user messages go to the end
    // In LTR: current user = right, other = left
    // In RTL: current user = left, other = right
    final alignment = isCurrentUser
        ? (isRtl ? Alignment.centerLeft : Alignment.centerRight)
        : (isRtl ? Alignment.centerRight : Alignment.centerLeft);

    final bubbleColor = isCurrentUser
        ? Theme.of(context).colorScheme.primary
        : Theme.of(context).colorScheme.surfaceVariant;

    final textColor = isCurrentUser
        ? Theme.of(context).colorScheme.onPrimary
        : Theme.of(context).colorScheme.onSurfaceVariant;

    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
      child: AnimatedAlign(
        duration: const Duration(milliseconds: 300),
        curve: Curves.easeOutCubic,
        alignment: alignment,
        child: Semantics(
          label: l10n.chatMessageAccessibility(
            sender,
            message,
            _formatTime(context, timestamp),
          ),
          child: ConstrainedBox(
            constraints: BoxConstraints(
              maxWidth: MediaQuery.of(context).size.width * 0.75,
            ),
            child: Column(
              crossAxisAlignment: isCurrentUser
                  ? (isRtl ? CrossAxisAlignment.start : CrossAxisAlignment.end)
                  : (isRtl ? CrossAxisAlignment.end : CrossAxisAlignment.start),
              children: [
                if (!isCurrentUser)
                  Padding(
                    padding: const EdgeInsets.only(bottom: 4),
                    child: Text(
                      sender,
                      style: Theme.of(context).textTheme.labelSmall?.copyWith(
                        color: Theme.of(context).colorScheme.outline,
                      ),
                    ),
                  ),
                Container(
                  padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
                  decoration: BoxDecoration(
                    color: bubbleColor,
                    borderRadius: BorderRadius.only(
                      topLeft: const Radius.circular(16),
                      topRight: const Radius.circular(16),
                      bottomLeft: Radius.circular(isCurrentUser ? 16 : 4),
                      bottomRight: Radius.circular(isCurrentUser ? 4 : 16),
                    ),
                  ),
                  child: Text(
                    message,
                    style: TextStyle(color: textColor),
                  ),
                ),
                Padding(
                  padding: const EdgeInsets.only(top: 4),
                  child: Text(
                    _formatTime(context, timestamp),
                    style: Theme.of(context).textTheme.labelSmall?.copyWith(
                      color: Theme.of(context).colorScheme.outline,
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }

  String _formatTime(BuildContext context, DateTime time) {
    final locale = Localizations.localeOf(context);
    return '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}';
  }
}

// Chat screen example
class LocalizedChatScreen extends StatefulWidget {
  const LocalizedChatScreen({super.key});

  @override
  State<LocalizedChatScreen> createState() => _LocalizedChatScreenState();
}

class _LocalizedChatScreenState extends State<LocalizedChatScreen> {
  final _messageController = TextEditingController();
  final List<_ChatMessage> _messages = [];

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

  void _sendMessage(AppLocalizations l10n) {
    if (_messageController.text.trim().isEmpty) return;

    setState(() {
      _messages.add(_ChatMessage(
        message: _messageController.text,
        sender: l10n.you,
        isCurrentUser: true,
        timestamp: DateTime.now(),
      ));
    });

    _messageController.clear();

    // Simulate reply
    Future.delayed(const Duration(seconds: 1), () {
      if (mounted) {
        setState(() {
          _messages.add(_ChatMessage(
            message: l10n.autoReplyMessage,
            sender: l10n.supportAgent,
            isCurrentUser: false,
            timestamp: DateTime.now(),
          ));
        });
      }
    });
  }

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

    return Scaffold(
      appBar: AppBar(
        title: Text(l10n.chatTitle),
      ),
      body: Column(
        children: [
          Expanded(
            child: _messages.isEmpty
                ? Center(
                    child: Text(
                      l10n.startConversation,
                      style: Theme.of(context).textTheme.bodyLarge?.copyWith(
                        color: Theme.of(context).colorScheme.outline,
                      ),
                    ),
                  )
                : ListView.builder(
                    padding: const EdgeInsets.symmetric(vertical: 16),
                    itemCount: _messages.length,
                    itemBuilder: (context, index) {
                      final msg = _messages[index];
                      return LocalizedChatMessage(
                        message: msg.message,
                        sender: msg.sender,
                        isCurrentUser: msg.isCurrentUser,
                        timestamp: msg.timestamp,
                      );
                    },
                  ),
          ),
          Container(
            padding: const EdgeInsets.all(16),
            decoration: BoxDecoration(
              color: Theme.of(context).colorScheme.surface,
              boxShadow: [
                BoxShadow(
                  color: Colors.black.withOpacity(0.1),
                  blurRadius: 4,
                  offset: const Offset(0, -2),
                ),
              ],
            ),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _messageController,
                    decoration: InputDecoration(
                      hintText: l10n.typeMessage,
                      border: OutlineInputBorder(
                        borderRadius: BorderRadius.circular(24),
                      ),
                      contentPadding: const EdgeInsets.symmetric(
                        horizontal: 16,
                        vertical: 12,
                      ),
                    ),
                    onSubmitted: (_) => _sendMessage(l10n),
                  ),
                ),
                const SizedBox(width: 8),
                FloatingActionButton(
                  mini: true,
                  onPressed: () => _sendMessage(l10n),
                  tooltip: l10n.sendMessage,
                  child: const Icon(Icons.send),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

class _ChatMessage {
  final String message;
  final String sender;
  final bool isCurrentUser;
  final DateTime timestamp;

  _ChatMessage({
    required this.message,
    required this.sender,
    required this.isCurrentUser,
    required this.timestamp,
  });
}

Loading Indicator with Alignment

Create a loading indicator that aligns based on content:

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

class LocalizedAlignedLoader extends StatefulWidget {
  const LocalizedAlignedLoader({super.key});

  @override
  State<LocalizedAlignedLoader> createState() => _LocalizedAlignedLoaderState();
}

class _LocalizedAlignedLoaderState extends State<LocalizedAlignedLoader> {
  bool _isLoading = false;
  bool _hasContent = false;

  Future<void> _loadContent() async {
    setState(() => _isLoading = true);

    await Future.delayed(const Duration(seconds: 2));

    setState(() {
      _isLoading = false;
      _hasContent = true;
    });
  }

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

    // Loader starts at center, then moves to top-start when content loads
    final loaderAlignment = _hasContent
        ? (isRtl ? Alignment.topRight : Alignment.topLeft)
        : Alignment.center;

    return Scaffold(
      appBar: AppBar(title: Text(l10n.contentLoaderTitle)),
      body: Stack(
        children: [
          // Main content
          if (_hasContent)
            Padding(
              padding: const EdgeInsets.only(top: 80),
              child: ListView.builder(
                itemCount: 20,
                itemBuilder: (context, index) {
                  return ListTile(
                    leading: CircleAvatar(child: Text('${index + 1}')),
                    title: Text(l10n.itemTitle(index + 1)),
                    subtitle: Text(l10n.itemSubtitle),
                  );
                },
              ),
            )
          else if (!_isLoading)
            Center(
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  Icon(
                    Icons.cloud_download,
                    size: 64,
                    color: Theme.of(context).colorScheme.outline,
                  ),
                  const SizedBox(height: 16),
                  Text(l10n.noContentYet),
                  const SizedBox(height: 16),
                  ElevatedButton(
                    onPressed: _loadContent,
                    child: Text(l10n.loadContent),
                  ),
                ],
              ),
            ),
          // Animated loader
          AnimatedAlign(
            duration: const Duration(milliseconds: 500),
            curve: Curves.easeOutCubic,
            alignment: loaderAlignment,
            child: AnimatedOpacity(
              duration: const Duration(milliseconds: 300),
              opacity: _isLoading ? 1.0 : (_hasContent ? 0.0 : 0.0),
              child: Semantics(
                label: l10n.loadingAccessibility,
                child: Container(
                  margin: const EdgeInsets.all(16),
                  padding: const EdgeInsets.all(16),
                  decoration: BoxDecoration(
                    color: Theme.of(context).colorScheme.primaryContainer,
                    borderRadius: BorderRadius.circular(12),
                  ),
                  child: Row(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      SizedBox(
                        width: 20,
                        height: 20,
                        child: CircularProgressIndicator(
                          strokeWidth: 2,
                          color: Theme.of(context).colorScheme.primary,
                        ),
                      ),
                      const SizedBox(width: 12),
                      Text(
                        l10n.loadingContent,
                        style: TextStyle(
                          color: Theme.of(context).colorScheme.onPrimaryContainer,
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

Focus Highlight Indicator

Create a focus indicator that moves between elements:

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

class LocalizedFocusIndicator extends StatefulWidget {
  const LocalizedFocusIndicator({super.key});

  @override
  State<LocalizedFocusIndicator> createState() => _LocalizedFocusIndicatorState();
}

class _LocalizedFocusIndicatorState extends State<LocalizedFocusIndicator> {
  int _focusedIndex = 0;
  final int _itemCount = 4;

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

    final menuItems = [
      _MenuItem(l10n.menuDashboard, Icons.dashboard),
      _MenuItem(l10n.menuAnalytics, Icons.analytics),
      _MenuItem(l10n.menuReports, Icons.description),
      _MenuItem(l10n.menuSettings, Icons.settings),
    ];

    return Scaffold(
      appBar: AppBar(title: Text(l10n.navigationTitle)),
      body: Row(
        children: [
          // Sidebar navigation
          Container(
            width: 200,
            color: Theme.of(context).colorScheme.surfaceVariant,
            child: Stack(
              children: [
                // Focus indicator
                AnimatedAlign(
                  duration: const Duration(milliseconds: 250),
                  curve: Curves.easeOutCubic,
                  alignment: Alignment(
                    isRtl ? 1.0 : -1.0,
                    -1.0 + (_focusedIndex * 2.0 / (_itemCount - 1)),
                  ),
                  child: Container(
                    width: 4,
                    height: 56,
                    decoration: BoxDecoration(
                      color: Theme.of(context).colorScheme.primary,
                      borderRadius: BorderRadius.circular(2),
                    ),
                  ),
                ),
                // Menu items
                Column(
                  children: menuItems.asMap().entries.map((entry) {
                    final index = entry.key;
                    final item = entry.value;
                    final isFocused = index == _focusedIndex;

                    return Semantics(
                      selected: isFocused,
                      button: true,
                      label: l10n.menuItemAccessibility(item.label, index + 1, menuItems.length),
                      child: InkWell(
                        onTap: () => setState(() => _focusedIndex = index),
                        child: AnimatedContainer(
                          duration: const Duration(milliseconds: 200),
                          height: 56,
                          padding: EdgeInsets.only(
                            left: isRtl ? 16 : (isFocused ? 20 : 16),
                            right: isRtl ? (isFocused ? 20 : 16) : 16,
                          ),
                          color: isFocused
                              ? Theme.of(context).colorScheme.primary.withOpacity(0.1)
                              : Colors.transparent,
                          child: Row(
                            children: [
                              Icon(
                                item.icon,
                                color: isFocused
                                    ? Theme.of(context).colorScheme.primary
                                    : Theme.of(context).colorScheme.onSurfaceVariant,
                              ),
                              const SizedBox(width: 12),
                              Text(
                                item.label,
                                style: TextStyle(
                                  color: isFocused
                                      ? Theme.of(context).colorScheme.primary
                                      : Theme.of(context).colorScheme.onSurfaceVariant,
                                  fontWeight: isFocused ? FontWeight.bold : FontWeight.normal,
                                ),
                              ),
                            ],
                          ),
                        ),
                      ),
                    );
                  }).toList(),
                ),
              ],
            ),
          ),
          // Content area
          Expanded(
            child: Center(
              child: Text(
                l10n.selectedMenuItem(menuItems[_focusedIndex].label),
                style: Theme.of(context).textTheme.headlineSmall,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class _MenuItem {
  final String label;
  final IconData icon;

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

Notification Badge Alignment

Create notification badges that align correctly:

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

class LocalizedNotificationBadge extends StatelessWidget {
  final int count;
  final Widget child;
  final bool showBadge;

  const LocalizedNotificationBadge({
    super.key,
    required this.count,
    required this.child,
    this.showBadge = true,
  });

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

    // Badge goes to top-end (top-right in LTR, top-left in RTL)
    final badgeAlignment = isRtl ? Alignment.topLeft : Alignment.topRight;

    return Stack(
      clipBehavior: Clip.none,
      children: [
        child,
        AnimatedAlign(
          duration: const Duration(milliseconds: 200),
          curve: Curves.easeOutBack,
          alignment: badgeAlignment,
          child: AnimatedScale(
            duration: const Duration(milliseconds: 200),
            scale: showBadge && count > 0 ? 1.0 : 0.0,
            child: Transform.translate(
              offset: Offset(isRtl ? -8 : 8, -8),
              child: Semantics(
                label: l10n.notificationBadgeAccessibility(count),
                child: Container(
                  padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
                  constraints: const BoxConstraints(minWidth: 20),
                  decoration: BoxDecoration(
                    color: Theme.of(context).colorScheme.error,
                    borderRadius: BorderRadius.circular(10),
                    border: Border.all(
                      color: Theme.of(context).colorScheme.surface,
                      width: 2,
                    ),
                  ),
                  child: Text(
                    count > 99 ? '99+' : count.toString(),
                    style: TextStyle(
                      color: Theme.of(context).colorScheme.onError,
                      fontSize: 10,
                      fontWeight: FontWeight.bold,
                    ),
                    textAlign: TextAlign.center,
                  ),
                ),
              ),
            ),
          ),
        ),
      ],
    );
  }
}

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

  @override
  State<NotificationBadgeDemo> createState() => _NotificationBadgeDemoState();
}

class _NotificationBadgeDemoState extends State<NotificationBadgeDemo> {
  int _notificationCount = 5;

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

    return Scaffold(
      appBar: AppBar(
        title: Text(l10n.notificationsTitle),
        actions: [
          LocalizedNotificationBadge(
            count: _notificationCount,
            child: IconButton(
              icon: const Icon(Icons.notifications),
              onPressed: () {},
              tooltip: l10n.viewNotifications,
            ),
          ),
          const SizedBox(width: 8),
        ],
      ),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            LocalizedNotificationBadge(
              count: _notificationCount,
              child: Container(
                width: 80,
                height: 80,
                decoration: BoxDecoration(
                  color: Theme.of(context).colorScheme.primaryContainer,
                  borderRadius: BorderRadius.circular(16),
                ),
                child: Icon(
                  Icons.mail,
                  size: 40,
                  color: Theme.of(context).colorScheme.primary,
                ),
              ),
            ),
            const SizedBox(height: 32),
            Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                IconButton(
                  onPressed: () => setState(() => _notificationCount++),
                  icon: const Icon(Icons.add),
                  tooltip: l10n.addNotification,
                ),
                IconButton(
                  onPressed: () => setState(() {
                    if (_notificationCount > 0) _notificationCount--;
                  }),
                  icon: const Icon(Icons.remove),
                  tooltip: l10n.removeNotification,
                ),
                IconButton(
                  onPressed: () => setState(() => _notificationCount = 0),
                  icon: const Icon(Icons.clear_all),
                  tooltip: l10n.clearAllNotifications,
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

Complete ARB File for AnimatedAlign

{
  "@@locale": "en",

  "alignmentDemoTitle": "Alignment Demo",
  "contentCenteredAccessibility": "Content is centered",
  "contentStartAccessibility": "Content is aligned to start",
  "centeredContent": "Centered",
  "alignedContent": "Start Aligned",
  "centerContent": "Center Content",
  "alignToStart": "Align to Start",

  "positionSelectorTitle": "Position Selector",
  "positionTopStart": "Top Start",
  "positionTopCenter": "Top Center",
  "positionTopEnd": "Top End",
  "positionCenterStart": "Center Start",
  "positionCenter": "Center",
  "positionCenterEnd": "Center End",
  "positionBottomStart": "Bottom Start",
  "positionBottomCenter": "Bottom Center",
  "positionBottomEnd": "Bottom End",
  "selectPosition": "Select position:",
  "contentAtPosition": "Content is at {position}",
  "@contentAtPosition": {
    "placeholders": {"position": {"type": "String"}}
  },
  "selectPositionAccessibility": "Move content to {position}",
  "@selectPositionAccessibility": {
    "placeholders": {"position": {"type": "String"}}
  },

  "chatTitle": "Chat",
  "startConversation": "Start a conversation",
  "typeMessage": "Type a message...",
  "sendMessage": "Send message",
  "you": "You",
  "supportAgent": "Support Agent",
  "autoReplyMessage": "Thanks for your message! We'll get back to you soon.",
  "chatMessageAccessibility": "{sender} said: {message} at {time}",
  "@chatMessageAccessibility": {
    "placeholders": {
      "sender": {"type": "String"},
      "message": {"type": "String"},
      "time": {"type": "String"}
    }
  },

  "contentLoaderTitle": "Content Loader",
  "noContentYet": "No content loaded yet",
  "loadContent": "Load Content",
  "loadingContent": "Loading...",
  "loadingAccessibility": "Loading content, please wait",
  "itemTitle": "Item {number}",
  "@itemTitle": {
    "placeholders": {"number": {"type": "int"}}
  },
  "itemSubtitle": "Tap for details",

  "navigationTitle": "Navigation",
  "menuDashboard": "Dashboard",
  "menuAnalytics": "Analytics",
  "menuReports": "Reports",
  "menuSettings": "Settings",
  "menuItemAccessibility": "{label}, menu item {current} of {total}",
  "@menuItemAccessibility": {
    "placeholders": {
      "label": {"type": "String"},
      "current": {"type": "int"},
      "total": {"type": "int"}
    }
  },
  "selectedMenuItem": "Selected: {item}",
  "@selectedMenuItem": {
    "placeholders": {"item": {"type": "String"}}
  },

  "notificationsTitle": "Notifications",
  "viewNotifications": "View notifications",
  "addNotification": "Add notification",
  "removeNotification": "Remove notification",
  "clearAllNotifications": "Clear all notifications",
  "notificationBadgeAccessibility": "{count} {count, plural, =0{no notifications} =1{notification} other{notifications}}",
  "@notificationBadgeAccessibility": {
    "placeholders": {"count": {"type": "int"}}
  }
}

Best Practices Summary

  1. Use directional alignment: Swap start/end alignments based on RTL state
  2. Avoid hardcoded left/right: Use AlignmentDirectional or calculate based on text direction
  3. Provide accessibility labels: Announce position changes for screen readers
  4. Choose appropriate curves: Use easeOutCubic for natural-feeling movement
  5. Keep durations reasonable: 250-500ms works well for alignment animations
  6. Consider content flow: Ensure aligned content doesn't overlap incorrectly
  7. Test with RTL layouts: Verify alignments flip correctly for Arabic/Hebrew
  8. Handle edge cases: Ensure content stays within bounds in all locales
  9. Combine with other animations: Use with opacity or scale for polished effects
  10. Support keyboard navigation: Ensure focus indicators align correctly

Conclusion

AnimatedAlign is essential for creating smooth, directionally-aware positioning animations in multilingual Flutter apps. By properly handling RTL layouts, providing accessibility feedback, and using directional terminology, you create intuitive experiences for users worldwide. The patterns shown here—position selectors, chat messages, loaders, and navigation indicators—can be adapted for any application requiring animated alignment transitions.

Remember to test your alignments with various locales to ensure content positions correctly regardless of the user's language and text direction preferences.