← Back to Blog

Flutter AnimatedCrossFade Localization: Content Switching, State Transitions, and Accessible Toggles

flutteranimatedcrossfadeanimationtransitionslocalizationaccessibility

Flutter AnimatedCrossFade Localization: Content Switching, State Transitions, and Accessible Toggles

AnimatedCrossFade smoothly transitions between two child widgets using a cross-fade animation. Proper localization ensures content switching, state toggles, and conditional displays work seamlessly across languages with proper accessibility support. This guide covers comprehensive strategies for localizing AnimatedCrossFade widgets in Flutter.

Understanding AnimatedCrossFade Localization

AnimatedCrossFade widgets require localization for:

  • Toggle states: Switching between two content variations
  • Expand/collapse content: Showing or hiding detailed information
  • Loading to content transitions: Fading from loader to actual content
  • Error state switches: Transitioning between success and error states
  • View mode toggles: Grid/list view switches with localized labels
  • Accessibility announcements: Screen reader notifications for content changes

Basic AnimatedCrossFade with Localized States

Start with a simple cross-fade toggle with proper accessibility:

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

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

  @override
  State<LocalizedContentToggle> createState() => _LocalizedContentToggleState();
}

class _LocalizedContentToggleState extends State<LocalizedContentToggle> {
  bool _showDetails = false;

  void _toggle() {
    setState(() => _showDetails = !_showDetails);

    // Announce state change to screen readers
    final l10n = AppLocalizations.of(context)!;
    SemanticsService.announce(
      _showDetails ? l10n.detailsExpanded : l10n.detailsCollapsed,
      Directionality.of(context),
    );
  }

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

    return Card(
      margin: const EdgeInsets.all(16),
      child: Column(
        children: [
          ListTile(
            title: Text(l10n.productTitle),
            subtitle: Text(l10n.productSubtitle),
            trailing: Semantics(
              button: true,
              label: _showDetails
                  ? l10n.hideDetailsButton
                  : l10n.showDetailsButton,
              child: IconButton(
                onPressed: _toggle,
                icon: AnimatedRotation(
                  turns: _showDetails ? 0.5 : 0,
                  duration: const Duration(milliseconds: 300),
                  child: const Icon(Icons.expand_more),
                ),
              ),
            ),
          ),
          AnimatedCrossFade(
            firstChild: const SizedBox.shrink(),
            secondChild: _buildDetails(l10n),
            crossFadeState: _showDetails
                ? CrossFadeState.showSecond
                : CrossFadeState.showFirst,
            duration: const Duration(milliseconds: 300),
            sizeCurve: Curves.easeInOut,
            firstCurve: Curves.easeOut,
            secondCurve: Curves.easeIn,
          ),
        ],
      ),
    );
  }

  Widget _buildDetails(AppLocalizations l10n) {
    return Semantics(
      container: true,
      label: l10n.productDetailsSection,
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Divider(),
            const SizedBox(height: 8),
            Text(
              l10n.productDescription,
              style: Theme.of(context).textTheme.bodyMedium,
            ),
            const SizedBox(height: 16),
            Row(
              children: [
                Expanded(
                  child: OutlinedButton.icon(
                    onPressed: () {},
                    icon: const Icon(Icons.add_shopping_cart),
                    label: Text(l10n.addToCart),
                  ),
                ),
                const SizedBox(width: 12),
                Expanded(
                  child: FilledButton.icon(
                    onPressed: () {},
                    icon: const Icon(Icons.shopping_bag),
                    label: Text(l10n.buyNow),
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

ARB File Structure for AnimatedCrossFade

{
  "productTitle": "Premium Wireless Headphones",
  "@productTitle": {
    "description": "Product title"
  },
  "productSubtitle": "High-fidelity audio experience",
  "@productSubtitle": {
    "description": "Product subtitle"
  },
  "productDescription": "Experience crystal-clear audio with our premium wireless headphones. Features active noise cancellation, 30-hour battery life, and comfortable over-ear design perfect for long listening sessions.",
  "@productDescription": {
    "description": "Detailed product description"
  },
  "productDetailsSection": "Product details section",
  "@productDetailsSection": {
    "description": "Accessibility label for details section"
  },
  "showDetailsButton": "Show product details",
  "@showDetailsButton": {
    "description": "Show details button accessibility label"
  },
  "hideDetailsButton": "Hide product details",
  "@hideDetailsButton": {
    "description": "Hide details button accessibility label"
  },
  "detailsExpanded": "Product details expanded",
  "@detailsExpanded": {
    "description": "Announcement when details are shown"
  },
  "detailsCollapsed": "Product details collapsed",
  "@detailsCollapsed": {
    "description": "Announcement when details are hidden"
  },
  "addToCart": "Add to Cart",
  "@addToCart": {
    "description": "Add to cart button"
  },
  "buyNow": "Buy Now",
  "@buyNow": {
    "description": "Buy now button"
  }
}

View Mode Toggle with Cross-Fade

Create a grid/list view toggle with smooth transitions:

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

class LocalizedViewModeToggle extends StatefulWidget {
  final List<ProductItem> products;

  const LocalizedViewModeToggle({super.key, required this.products});

  @override
  State<LocalizedViewModeToggle> createState() => _LocalizedViewModeToggleState();
}

class _LocalizedViewModeToggleState extends State<LocalizedViewModeToggle> {
  bool _isGridView = true;

  void _toggleViewMode() {
    setState(() => _isGridView = !_isGridView);

    final l10n = AppLocalizations.of(context)!;
    SemanticsService.announce(
      _isGridView ? l10n.gridViewEnabled : l10n.listViewEnabled,
      Directionality.of(context),
    );
  }

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

    return Scaffold(
      appBar: AppBar(
        title: Text(l10n.productsTitle),
        actions: [
          Semantics(
            button: true,
            label: _isGridView
                ? l10n.switchToListView
                : l10n.switchToGridView,
            child: IconButton(
              onPressed: _toggleViewMode,
              icon: AnimatedCrossFade(
                firstChild: const Icon(Icons.view_list),
                secondChild: const Icon(Icons.grid_view),
                crossFadeState: _isGridView
                    ? CrossFadeState.showFirst
                    : CrossFadeState.showSecond,
                duration: const Duration(milliseconds: 200),
              ),
              tooltip: _isGridView ? l10n.listViewTooltip : l10n.gridViewTooltip,
            ),
          ),
        ],
      ),
      body: AnimatedCrossFade(
        firstChild: _buildGridView(l10n),
        secondChild: _buildListView(l10n),
        crossFadeState: _isGridView
            ? CrossFadeState.showFirst
            : CrossFadeState.showSecond,
        duration: const Duration(milliseconds: 300),
        layoutBuilder: (topChild, topChildKey, bottomChild, bottomChildKey) {
          return Stack(
            children: [
              Positioned.fill(key: bottomChildKey, child: bottomChild),
              Positioned.fill(key: topChildKey, child: topChild),
            ],
          );
        },
      ),
    );
  }

  Widget _buildGridView(AppLocalizations l10n) {
    return Semantics(
      label: l10n.gridViewLabel(widget.products.length),
      child: GridView.builder(
        padding: const EdgeInsets.all(16),
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
          childAspectRatio: 0.75,
          crossAxisSpacing: 16,
          mainAxisSpacing: 16,
        ),
        itemCount: widget.products.length,
        itemBuilder: (context, index) {
          final product = widget.products[index];
          return _ProductGridCard(product: product);
        },
      ),
    );
  }

  Widget _buildListView(AppLocalizations l10n) {
    return Semantics(
      label: l10n.listViewLabel(widget.products.length),
      child: ListView.separated(
        padding: const EdgeInsets.all(16),
        itemCount: widget.products.length,
        separatorBuilder: (_, __) => const Divider(),
        itemBuilder: (context, index) {
          final product = widget.products[index];
          return _ProductListTile(product: product);
        },
      ),
    );
  }
}

class _ProductGridCard extends StatelessWidget {
  final ProductItem product;

  const _ProductGridCard({required this.product});

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

    return Card(
      clipBehavior: Clip.antiAlias,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Expanded(
            flex: 3,
            child: Container(
              color: Colors.grey.shade200,
              child: Center(
                child: Icon(
                  Icons.image,
                  size: 48,
                  color: Colors.grey.shade400,
                ),
              ),
            ),
          ),
          Expanded(
            flex: 2,
            child: Padding(
              padding: const EdgeInsets.all(8),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    product.name,
                    style: Theme.of(context).textTheme.titleSmall,
                    maxLines: 2,
                    overflow: TextOverflow.ellipsis,
                  ),
                  const Spacer(),
                  Text(
                    l10n.priceLabel(product.price),
                    style: Theme.of(context).textTheme.titleMedium?.copyWith(
                      color: Theme.of(context).colorScheme.primary,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class _ProductListTile extends StatelessWidget {
  final ProductItem product;

  const _ProductListTile({required this.product});

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

    return ListTile(
      leading: Container(
        width: 56,
        height: 56,
        decoration: BoxDecoration(
          color: Colors.grey.shade200,
          borderRadius: BorderRadius.circular(8),
        ),
        child: Icon(
          Icons.image,
          color: Colors.grey.shade400,
        ),
      ),
      title: Text(product.name),
      subtitle: Text(product.description),
      trailing: Text(
        l10n.priceLabel(product.price),
        style: Theme.of(context).textTheme.titleMedium?.copyWith(
          color: Theme.of(context).colorScheme.primary,
          fontWeight: FontWeight.bold,
        ),
      ),
    );
  }
}

class ProductItem {
  final String id;
  final String name;
  final String description;
  final String price;

  ProductItem({
    required this.id,
    required this.name,
    required this.description,
    required this.price,
  });
}

Loading to Content Transition

Smooth transition from loading state to content:

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

class LocalizedLoadingTransition<T> extends StatelessWidget {
  final bool isLoading;
  final T? data;
  final String? error;
  final Widget Function(T data) builder;
  final VoidCallback? onRetry;

  const LocalizedLoadingTransition({
    super.key,
    required this.isLoading,
    this.data,
    this.error,
    required this.builder,
    this.onRetry,
  });

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

    // Determine which state to show
    CrossFadeState fadeState;
    Widget firstChild;
    Widget secondChild;

    if (isLoading) {
      fadeState = CrossFadeState.showFirst;
      firstChild = _buildLoadingState(context, l10n);
      secondChild = const SizedBox.shrink();
    } else if (error != null) {
      fadeState = CrossFadeState.showFirst;
      firstChild = _buildErrorState(context, l10n);
      secondChild = const SizedBox.shrink();
    } else if (data != null) {
      fadeState = CrossFadeState.showSecond;
      firstChild = _buildLoadingState(context, l10n);
      secondChild = builder(data as T);
    } else {
      fadeState = CrossFadeState.showFirst;
      firstChild = _buildEmptyState(context, l10n);
      secondChild = const SizedBox.shrink();
    }

    return AnimatedCrossFade(
      firstChild: firstChild,
      secondChild: secondChild,
      crossFadeState: fadeState,
      duration: const Duration(milliseconds: 400),
      sizeCurve: Curves.easeInOut,
    );
  }

  Widget _buildLoadingState(BuildContext context, AppLocalizations l10n) {
    return Semantics(
      liveRegion: true,
      label: l10n.loadingContent,
      child: Center(
        child: Padding(
          padding: const EdgeInsets.all(32),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const CircularProgressIndicator(),
              const SizedBox(height: 16),
              Text(
                l10n.loadingPleaseWait,
                style: Theme.of(context).textTheme.bodyLarge,
              ),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildErrorState(BuildContext context, AppLocalizations l10n) {
    return Semantics(
      liveRegion: true,
      label: l10n.errorOccurred,
      child: Center(
        child: Padding(
          padding: const EdgeInsets.all(32),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Icon(
                Icons.error_outline,
                size: 64,
                color: Theme.of(context).colorScheme.error,
              ),
              const SizedBox(height: 16),
              Text(
                l10n.errorTitle,
                style: Theme.of(context).textTheme.titleLarge?.copyWith(
                  color: Theme.of(context).colorScheme.error,
                ),
              ),
              const SizedBox(height: 8),
              Text(
                error ?? l10n.genericErrorMessage,
                textAlign: TextAlign.center,
                style: Theme.of(context).textTheme.bodyMedium,
              ),
              if (onRetry != null) ...[
                const SizedBox(height: 24),
                FilledButton.icon(
                  onPressed: onRetry,
                  icon: const Icon(Icons.refresh),
                  label: Text(l10n.retryButton),
                ),
              ],
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildEmptyState(BuildContext context, AppLocalizations l10n) {
    return Semantics(
      label: l10n.noDataAvailable,
      child: Center(
        child: Padding(
          padding: const EdgeInsets.all(32),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Icon(
                Icons.inbox_outlined,
                size: 64,
                color: Theme.of(context).colorScheme.outline,
              ),
              const SizedBox(height: 16),
              Text(
                l10n.emptyTitle,
                style: Theme.of(context).textTheme.titleLarge,
              ),
              const SizedBox(height: 8),
              Text(
                l10n.emptyMessage,
                textAlign: TextAlign.center,
                style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                  color: Theme.of(context).colorScheme.outline,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Success/Error State Toggle

Handle form submission states with cross-fade:

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

enum SubmissionState { idle, submitting, success, error }

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

  @override
  State<LocalizedSubmissionForm> createState() => _LocalizedSubmissionFormState();
}

class _LocalizedSubmissionFormState extends State<LocalizedSubmissionForm> {
  SubmissionState _state = SubmissionState.idle;
  final _emailController = TextEditingController();

  Future<void> _submit() async {
    setState(() => _state = SubmissionState.submitting);

    // Simulate API call
    await Future.delayed(const Duration(seconds: 2));

    // Randomly succeed or fail for demo
    final success = DateTime.now().second % 2 == 0;

    setState(() {
      _state = success ? SubmissionState.success : SubmissionState.error;
    });

    // Announce result
    final l10n = AppLocalizations.of(context)!;
    SemanticsService.announce(
      success ? l10n.subscriptionSuccess : l10n.subscriptionError,
      Directionality.of(context),
    );
  }

  void _reset() {
    setState(() {
      _state = SubmissionState.idle;
      _emailController.clear();
    });
  }

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

    return Card(
      margin: const EdgeInsets.all(16),
      child: Padding(
        padding: const EdgeInsets.all(24),
        child: AnimatedCrossFade(
          firstChild: _buildForm(l10n),
          secondChild: _buildResult(l10n),
          crossFadeState: _state == SubmissionState.success ||
                  _state == SubmissionState.error
              ? CrossFadeState.showSecond
              : CrossFadeState.showFirst,
          duration: const Duration(milliseconds: 400),
          sizeCurve: Curves.easeInOut,
        ),
      ),
    );
  }

  Widget _buildForm(AppLocalizations l10n) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        Text(
          l10n.newsletterTitle,
          style: Theme.of(context).textTheme.titleLarge,
        ),
        const SizedBox(height: 8),
        Text(
          l10n.newsletterSubtitle,
          style: Theme.of(context).textTheme.bodyMedium?.copyWith(
            color: Theme.of(context).colorScheme.outline,
          ),
        ),
        const SizedBox(height: 24),
        TextField(
          controller: _emailController,
          decoration: InputDecoration(
            labelText: l10n.emailLabel,
            hintText: l10n.emailHint,
            prefixIcon: const Icon(Icons.email_outlined),
            border: const OutlineInputBorder(),
          ),
          keyboardType: TextInputType.emailAddress,
          enabled: _state != SubmissionState.submitting,
        ),
        const SizedBox(height: 16),
        AnimatedCrossFade(
          firstChild: FilledButton.icon(
            onPressed: _submit,
            icon: const Icon(Icons.send),
            label: Text(l10n.subscribeButton),
          ),
          secondChild: FilledButton.icon(
            onPressed: null,
            icon: const SizedBox(
              width: 16,
              height: 16,
              child: CircularProgressIndicator(
                strokeWidth: 2,
                color: Colors.white,
              ),
            ),
            label: Text(l10n.subscribingButton),
          ),
          crossFadeState: _state == SubmissionState.submitting
              ? CrossFadeState.showSecond
              : CrossFadeState.showFirst,
          duration: const Duration(milliseconds: 200),
        ),
      ],
    );
  }

  Widget _buildResult(AppLocalizations l10n) {
    final isSuccess = _state == SubmissionState.success;

    return Column(
      children: [
        Icon(
          isSuccess ? Icons.check_circle : Icons.error,
          size: 64,
          color: isSuccess ? Colors.green : Theme.of(context).colorScheme.error,
        ),
        const SizedBox(height: 16),
        Text(
          isSuccess ? l10n.subscriptionSuccessTitle : l10n.subscriptionErrorTitle,
          style: Theme.of(context).textTheme.titleLarge?.copyWith(
            color: isSuccess ? Colors.green : Theme.of(context).colorScheme.error,
          ),
          textAlign: TextAlign.center,
        ),
        const SizedBox(height: 8),
        Text(
          isSuccess ? l10n.subscriptionSuccessMessage : l10n.subscriptionErrorMessage,
          style: Theme.of(context).textTheme.bodyMedium,
          textAlign: TextAlign.center,
        ),
        const SizedBox(height: 24),
        OutlinedButton.icon(
          onPressed: _reset,
          icon: Icon(isSuccess ? Icons.done : Icons.refresh),
          label: Text(isSuccess ? l10n.doneButton : l10n.tryAgainButton),
        ),
      ],
    );
  }

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

FAQ Accordion with Cross-Fade

Create an FAQ section with expanding answers:

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

class LocalizedFAQSection extends StatelessWidget {
  final List<FAQItem> items;

  const LocalizedFAQSection({super.key, required this.items});

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

    return Semantics(
      label: l10n.faqSectionLabel,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Padding(
            padding: const EdgeInsets.all(16),
            child: Text(
              l10n.faqTitle,
              style: Theme.of(context).textTheme.headlineSmall,
            ),
          ),
          ...items.map((item) => _FAQTile(item: item)),
        ],
      ),
    );
  }
}

class _FAQTile extends StatefulWidget {
  final FAQItem item;

  const _FAQTile({required this.item});

  @override
  State<_FAQTile> createState() => _FAQTileState();
}

class _FAQTileState extends State<_FAQTile> {
  bool _isExpanded = false;

  void _toggle() {
    setState(() => _isExpanded = !_isExpanded);

    final l10n = AppLocalizations.of(context)!;
    SemanticsService.announce(
      _isExpanded
          ? l10n.faqAnswerExpanded(widget.item.question)
          : l10n.faqAnswerCollapsed(widget.item.question),
      Directionality.of(context),
    );
  }

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

    return Card(
      margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
      child: Column(
        children: [
          Semantics(
            button: true,
            expanded: _isExpanded,
            label: l10n.faqQuestionLabel(widget.item.question),
            child: InkWell(
              onTap: _toggle,
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Row(
                  children: [
                    Expanded(
                      child: Text(
                        widget.item.question,
                        style: Theme.of(context).textTheme.titleMedium?.copyWith(
                          fontWeight: FontWeight.w500,
                        ),
                      ),
                    ),
                    AnimatedRotation(
                      turns: _isExpanded ? 0.5 : 0,
                      duration: const Duration(milliseconds: 200),
                      child: Icon(
                        Icons.expand_more,
                        color: Theme.of(context).colorScheme.primary,
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ),
          AnimatedCrossFade(
            firstChild: const SizedBox.shrink(),
            secondChild: Container(
              width: double.infinity,
              padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const Divider(),
                  const SizedBox(height: 8),
                  Semantics(
                    label: l10n.faqAnswerLabel,
                    child: Text(
                      widget.item.answer,
                      style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                        color: Theme.of(context).colorScheme.onSurfaceVariant,
                      ),
                    ),
                  ),
                  if (widget.item.links != null && widget.item.links!.isNotEmpty) ...[
                    const SizedBox(height: 12),
                    Text(
                      l10n.relatedLinks,
                      style: Theme.of(context).textTheme.labelMedium?.copyWith(
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    ...widget.item.links!.map((link) => TextButton(
                      onPressed: () {},
                      style: TextButton.styleFrom(
                        padding: EdgeInsets.zero,
                        minimumSize: Size.zero,
                        tapTargetSize: MaterialTapTargetSize.shrinkWrap,
                      ),
                      child: Text(link),
                    )),
                  ],
                ],
              ),
            ),
            crossFadeState: _isExpanded
                ? CrossFadeState.showSecond
                : CrossFadeState.showFirst,
            duration: const Duration(milliseconds: 300),
            sizeCurve: Curves.easeInOut,
          ),
        ],
      ),
    );
  }
}

class FAQItem {
  final String question;
  final String answer;
  final List<String>? links;

  FAQItem({
    required this.question,
    required this.answer,
    this.links,
  });
}

Login/Register Toggle

Toggle between login and registration forms:

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

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

  @override
  State<LocalizedAuthToggle> createState() => _LocalizedAuthToggleState();
}

class _LocalizedAuthToggleState extends State<LocalizedAuthToggle> {
  bool _isLogin = true;

  void _toggleMode() {
    setState(() => _isLogin = !_isLogin);

    final l10n = AppLocalizations.of(context)!;
    SemanticsService.announce(
      _isLogin ? l10n.loginFormActive : l10n.registerFormActive,
      Directionality.of(context),
    );
  }

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

    return SingleChildScrollView(
      padding: const EdgeInsets.all(24),
      child: Column(
        children: [
          // Toggle tabs
          _buildToggleTabs(l10n),
          const SizedBox(height: 32),
          // Form content
          AnimatedCrossFade(
            firstChild: _buildLoginForm(l10n),
            secondChild: _buildRegisterForm(l10n),
            crossFadeState: _isLogin
                ? CrossFadeState.showFirst
                : CrossFadeState.showSecond,
            duration: const Duration(milliseconds: 300),
            sizeCurve: Curves.easeInOut,
            layoutBuilder: (topChild, topChildKey, bottomChild, bottomChildKey) {
              return Stack(
                clipBehavior: Clip.none,
                children: [
                  Positioned(
                    key: bottomChildKey,
                    child: bottomChild,
                  ),
                  Positioned(
                    key: topChildKey,
                    child: topChild,
                  ),
                ],
              );
            },
          ),
        ],
      ),
    );
  }

  Widget _buildToggleTabs(AppLocalizations l10n) {
    return Container(
      padding: const EdgeInsets.all(4),
      decoration: BoxDecoration(
        color: Theme.of(context).colorScheme.surfaceVariant,
        borderRadius: BorderRadius.circular(12),
      ),
      child: Row(
        children: [
          Expanded(
            child: _ToggleTab(
              label: l10n.loginTab,
              isActive: _isLogin,
              onTap: () {
                if (!_isLogin) _toggleMode();
              },
            ),
          ),
          Expanded(
            child: _ToggleTab(
              label: l10n.registerTab,
              isActive: !_isLogin,
              onTap: () {
                if (_isLogin) _toggleMode();
              },
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildLoginForm(AppLocalizations l10n) {
    return Semantics(
      label: l10n.loginFormLabel,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Text(
            l10n.welcomeBack,
            style: Theme.of(context).textTheme.headlineMedium,
          ),
          const SizedBox(height: 8),
          Text(
            l10n.loginSubtitle,
            style: Theme.of(context).textTheme.bodyMedium?.copyWith(
              color: Theme.of(context).colorScheme.outline,
            ),
          ),
          const SizedBox(height: 32),
          TextField(
            decoration: InputDecoration(
              labelText: l10n.emailLabel,
              prefixIcon: const Icon(Icons.email_outlined),
              border: const OutlineInputBorder(),
            ),
          ),
          const SizedBox(height: 16),
          TextField(
            obscureText: true,
            decoration: InputDecoration(
              labelText: l10n.passwordLabel,
              prefixIcon: const Icon(Icons.lock_outlined),
              border: const OutlineInputBorder(),
            ),
          ),
          const SizedBox(height: 8),
          Align(
            alignment: AlignmentDirectional.centerEnd,
            child: TextButton(
              onPressed: () {},
              child: Text(l10n.forgotPassword),
            ),
          ),
          const SizedBox(height: 24),
          FilledButton(
            onPressed: () {},
            child: Text(l10n.loginButton),
          ),
        ],
      ),
    );
  }

  Widget _buildRegisterForm(AppLocalizations l10n) {
    return Semantics(
      label: l10n.registerFormLabel,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Text(
            l10n.createAccount,
            style: Theme.of(context).textTheme.headlineMedium,
          ),
          const SizedBox(height: 8),
          Text(
            l10n.registerSubtitle,
            style: Theme.of(context).textTheme.bodyMedium?.copyWith(
              color: Theme.of(context).colorScheme.outline,
            ),
          ),
          const SizedBox(height: 32),
          TextField(
            decoration: InputDecoration(
              labelText: l10n.fullNameLabel,
              prefixIcon: const Icon(Icons.person_outlined),
              border: const OutlineInputBorder(),
            ),
          ),
          const SizedBox(height: 16),
          TextField(
            decoration: InputDecoration(
              labelText: l10n.emailLabel,
              prefixIcon: const Icon(Icons.email_outlined),
              border: const OutlineInputBorder(),
            ),
          ),
          const SizedBox(height: 16),
          TextField(
            obscureText: true,
            decoration: InputDecoration(
              labelText: l10n.passwordLabel,
              prefixIcon: const Icon(Icons.lock_outlined),
              border: const OutlineInputBorder(),
            ),
          ),
          const SizedBox(height: 16),
          TextField(
            obscureText: true,
            decoration: InputDecoration(
              labelText: l10n.confirmPasswordLabel,
              prefixIcon: const Icon(Icons.lock_outlined),
              border: const OutlineInputBorder(),
            ),
          ),
          const SizedBox(height: 24),
          FilledButton(
            onPressed: () {},
            child: Text(l10n.registerButton),
          ),
        ],
      ),
    );
  }
}

class _ToggleTab extends StatelessWidget {
  final String label;
  final bool isActive;
  final VoidCallback onTap;

  const _ToggleTab({
    required this.label,
    required this.isActive,
    required this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    return Semantics(
      selected: isActive,
      button: true,
      child: GestureDetector(
        onTap: onTap,
        child: AnimatedContainer(
          duration: const Duration(milliseconds: 200),
          padding: const EdgeInsets.symmetric(vertical: 12),
          decoration: BoxDecoration(
            color: isActive
                ? Theme.of(context).colorScheme.surface
                : Colors.transparent,
            borderRadius: BorderRadius.circular(8),
            boxShadow: isActive
                ? [
                    BoxShadow(
                      color: Colors.black.withOpacity(0.1),
                      blurRadius: 4,
                      offset: const Offset(0, 2),
                    ),
                  ]
                : null,
          ),
          child: Center(
            child: Text(
              label,
              style: TextStyle(
                fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
                color: isActive
                    ? Theme.of(context).colorScheme.primary
                    : Theme.of(context).colorScheme.onSurfaceVariant,
              ),
            ),
          ),
        ),
      ),
    );
  }
}

Complete ARB File for AnimatedCrossFade

{
  "@@locale": "en",

  "productsTitle": "Products",
  "@productsTitle": {
    "description": "Products page title"
  },
  "gridViewEnabled": "Switched to grid view",
  "@gridViewEnabled": {
    "description": "Announcement when grid view is enabled"
  },
  "listViewEnabled": "Switched to list view",
  "@listViewEnabled": {
    "description": "Announcement when list view is enabled"
  },
  "switchToListView": "Switch to list view",
  "@switchToListView": {
    "description": "Button to switch to list view"
  },
  "switchToGridView": "Switch to grid view",
  "@switchToGridView": {
    "description": "Button to switch to grid view"
  },
  "listViewTooltip": "List view",
  "@listViewTooltip": {
    "description": "List view tooltip"
  },
  "gridViewTooltip": "Grid view",
  "@gridViewTooltip": {
    "description": "Grid view tooltip"
  },
  "gridViewLabel": "Grid view showing {count} products",
  "@gridViewLabel": {
    "description": "Grid view accessibility label",
    "placeholders": {
      "count": {"type": "int"}
    }
  },
  "listViewLabel": "List view showing {count} products",
  "@listViewLabel": {
    "description": "List view accessibility label",
    "placeholders": {
      "count": {"type": "int"}
    }
  },
  "priceLabel": "${price}",
  "@priceLabel": {
    "description": "Price display",
    "placeholders": {
      "price": {"type": "String"}
    }
  },

  "loadingContent": "Loading content",
  "@loadingContent": {
    "description": "Loading state accessibility label"
  },
  "loadingPleaseWait": "Loading, please wait...",
  "@loadingPleaseWait": {
    "description": "Loading message"
  },
  "errorOccurred": "An error occurred",
  "@errorOccurred": {
    "description": "Error accessibility label"
  },
  "errorTitle": "Something went wrong",
  "@errorTitle": {
    "description": "Error title"
  },
  "genericErrorMessage": "We couldn't load the content. Please try again.",
  "@genericErrorMessage": {
    "description": "Generic error message"
  },
  "retryButton": "Try Again",
  "@retryButton": {
    "description": "Retry button"
  },
  "noDataAvailable": "No data available",
  "@noDataAvailable": {
    "description": "Empty state accessibility label"
  },
  "emptyTitle": "Nothing here yet",
  "@emptyTitle": {
    "description": "Empty state title"
  },
  "emptyMessage": "Content will appear here once it's available.",
  "@emptyMessage": {
    "description": "Empty state message"
  },

  "newsletterTitle": "Stay Updated",
  "@newsletterTitle": {
    "description": "Newsletter section title"
  },
  "newsletterSubtitle": "Subscribe to our newsletter for the latest updates and exclusive offers.",
  "@newsletterSubtitle": {
    "description": "Newsletter section subtitle"
  },
  "emailLabel": "Email address",
  "@emailLabel": {
    "description": "Email field label"
  },
  "emailHint": "you@example.com",
  "@emailHint": {
    "description": "Email field hint"
  },
  "subscribeButton": "Subscribe",
  "@subscribeButton": {
    "description": "Subscribe button"
  },
  "subscribingButton": "Subscribing...",
  "@subscribingButton": {
    "description": "Subscribing in progress button"
  },
  "subscriptionSuccess": "Successfully subscribed to newsletter",
  "@subscriptionSuccess": {
    "description": "Subscription success announcement"
  },
  "subscriptionError": "Failed to subscribe to newsletter",
  "@subscriptionError": {
    "description": "Subscription error announcement"
  },
  "subscriptionSuccessTitle": "You're subscribed!",
  "@subscriptionSuccessTitle": {
    "description": "Success title"
  },
  "subscriptionSuccessMessage": "Thank you for subscribing. Check your email for a confirmation.",
  "@subscriptionSuccessMessage": {
    "description": "Success message"
  },
  "subscriptionErrorTitle": "Subscription failed",
  "@subscriptionErrorTitle": {
    "description": "Error title"
  },
  "subscriptionErrorMessage": "We couldn't complete your subscription. Please try again.",
  "@subscriptionErrorMessage": {
    "description": "Error message"
  },
  "doneButton": "Done",
  "@doneButton": {
    "description": "Done button"
  },
  "tryAgainButton": "Try Again",
  "@tryAgainButton": {
    "description": "Try again button"
  },

  "faqTitle": "Frequently Asked Questions",
  "@faqTitle": {
    "description": "FAQ section title"
  },
  "faqSectionLabel": "Frequently asked questions section",
  "@faqSectionLabel": {
    "description": "FAQ section accessibility label"
  },
  "faqQuestionLabel": "Question: {question}",
  "@faqQuestionLabel": {
    "description": "FAQ question accessibility label",
    "placeholders": {
      "question": {"type": "String"}
    }
  },
  "faqAnswerLabel": "Answer",
  "@faqAnswerLabel": {
    "description": "FAQ answer accessibility label"
  },
  "faqAnswerExpanded": "Answer for {question} is now visible",
  "@faqAnswerExpanded": {
    "description": "FAQ answer expanded announcement",
    "placeholders": {
      "question": {"type": "String"}
    }
  },
  "faqAnswerCollapsed": "Answer for {question} is now hidden",
  "@faqAnswerCollapsed": {
    "description": "FAQ answer collapsed announcement",
    "placeholders": {
      "question": {"type": "String"}
    }
  },
  "relatedLinks": "Related links:",
  "@relatedLinks": {
    "description": "Related links label"
  },

  "loginTab": "Login",
  "@loginTab": {
    "description": "Login tab label"
  },
  "registerTab": "Register",
  "@registerTab": {
    "description": "Register tab label"
  },
  "loginFormActive": "Login form is now active",
  "@loginFormActive": {
    "description": "Login form active announcement"
  },
  "registerFormActive": "Registration form is now active",
  "@registerFormActive": {
    "description": "Register form active announcement"
  },
  "loginFormLabel": "Login form",
  "@loginFormLabel": {
    "description": "Login form accessibility label"
  },
  "registerFormLabel": "Registration form",
  "@registerFormLabel": {
    "description": "Registration form accessibility label"
  },
  "welcomeBack": "Welcome back",
  "@welcomeBack": {
    "description": "Login form title"
  },
  "loginSubtitle": "Enter your credentials to access your account",
  "@loginSubtitle": {
    "description": "Login form subtitle"
  },
  "createAccount": "Create account",
  "@createAccount": {
    "description": "Register form title"
  },
  "registerSubtitle": "Fill in your details to get started",
  "@registerSubtitle": {
    "description": "Register form subtitle"
  },
  "fullNameLabel": "Full name",
  "@fullNameLabel": {
    "description": "Full name field label"
  },
  "passwordLabel": "Password",
  "@passwordLabel": {
    "description": "Password field label"
  },
  "confirmPasswordLabel": "Confirm password",
  "@confirmPasswordLabel": {
    "description": "Confirm password field label"
  },
  "forgotPassword": "Forgot password?",
  "@forgotPassword": {
    "description": "Forgot password link"
  },
  "loginButton": "Sign in",
  "@loginButton": {
    "description": "Login button"
  },
  "registerButton": "Create account",
  "@registerButton": {
    "description": "Register button"
  }
}

Best Practices Summary

  1. Announce state changes: Use SemanticsService.announce when content switches
  2. Use custom layoutBuilder: Handle size transitions smoothly with Stack
  3. Set appropriate durations: 200-400ms works well for most cross-fades
  4. Apply sizeCurve: Use easeInOut for natural size transitions
  5. Wrap in Semantics: Provide labels for both states
  6. Handle focus correctly: Manage focus when switching between forms
  7. Support RTL layouts: Test with right-to-left languages
  8. Use firstCurve and secondCurve: Fine-tune individual fade animations
  9. Consider accessibility: Ensure screen readers can navigate both states
  10. Test transitions: Verify smooth animations across different content sizes

Conclusion

Proper AnimatedCrossFade localization ensures smooth, accessible content transitions across all languages. By announcing state changes, providing comprehensive accessibility labels, and handling bidirectional layouts, you create polished cross-fade animations that work well for users worldwide. The patterns shown here—view mode toggles, loading transitions, form state changes, and FAQ accordions—can be adapted to any Flutter application requiring animated content switching with localization support.

Remember to test your cross-fade animations with screen readers to verify that content changes are properly announced for all your supported languages.