← Back to Blog

Flutter DecoratedBoxTransition Localization: Animated Visual Effects for Multilingual Apps

flutterdecoratedboxtransitionanimationdecorationlocalizationthemes

Flutter DecoratedBoxTransition Localization: Animated Visual Effects for Multilingual Apps

DecoratedBoxTransition provides explicit control over decoration animations using an Animation controller. Unlike AnimatedContainer which handles decoration changes internally, DecoratedBoxTransition gives you precise control over timing, curves, and synchronization when animating borders, shadows, gradients, and backgrounds. This guide covers comprehensive strategies for localizing DecoratedBoxTransition widgets in Flutter multilingual applications.

Understanding DecoratedBoxTransition Localization

DecoratedBoxTransition widgets require localization for:

  • Selection states: Visual feedback for selected items
  • Validation feedback: Form field state indicators
  • Theme switching: Dark/light mode transitions
  • Focus indicators: Keyboard navigation highlights
  • Status indicators: Progress and completion states
  • Interactive cards: Hover and press effects

Basic DecoratedBoxTransition with Localized Content

Start with a simple selection card:

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

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

  @override
  State<LocalizedSelectionCard> createState() => _LocalizedSelectionCardState();
}

class _LocalizedSelectionCardState extends State<LocalizedSelectionCard>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<Decoration> _decorationAnimation;
  bool _isSelected = false;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 250),
    );
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _updateAnimation();
  }

  void _updateAnimation() {
    final colorScheme = Theme.of(context).colorScheme;

    _decorationAnimation = DecorationTween(
      begin: BoxDecoration(
        color: colorScheme.surface,
        borderRadius: BorderRadius.circular(12),
        border: Border.all(
          color: colorScheme.outline.withOpacity(0.5),
          width: 1,
        ),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.05),
            blurRadius: 4,
            offset: const Offset(0, 2),
          ),
        ],
      ),
      end: BoxDecoration(
        color: colorScheme.primaryContainer,
        borderRadius: BorderRadius.circular(12),
        border: Border.all(
          color: colorScheme.primary,
          width: 2,
        ),
        boxShadow: [
          BoxShadow(
            color: colorScheme.primary.withOpacity(0.3),
            blurRadius: 12,
            offset: const Offset(0, 4),
          ),
        ],
      ),
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeInOut,
    ));
  }

  void _toggleSelection() {
    setState(() {
      _isSelected = !_isSelected;
      if (_isSelected) {
        _controller.forward();
      } else {
        _controller.reverse();
      }
    });
  }

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

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

    return Scaffold(
      appBar: AppBar(title: Text(l10n.selectionCardTitle)),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Semantics(
          button: true,
          selected: _isSelected,
          label: l10n.planCardAccessibility(
            l10n.premiumPlanTitle,
            _isSelected ? l10n.selected : l10n.notSelected,
          ),
          child: GestureDetector(
            onTap: _toggleSelection,
            child: DecoratedBoxTransition(
              decoration: _decorationAnimation,
              child: Padding(
                padding: const EdgeInsets.all(20),
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Row(
                      children: [
                        Container(
                          padding: const EdgeInsets.all(12),
                          decoration: BoxDecoration(
                            color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
                            borderRadius: BorderRadius.circular(8),
                          ),
                          child: Icon(
                            Icons.star,
                            color: Theme.of(context).colorScheme.primary,
                          ),
                        ),
                        const SizedBox(width: 16),
                        Expanded(
                          child: Column(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                              Text(
                                l10n.premiumPlanTitle,
                                style: Theme.of(context).textTheme.titleLarge,
                              ),
                              Text(
                                l10n.premiumPlanPrice,
                                style: Theme.of(context).textTheme.titleMedium?.copyWith(
                                  color: Theme.of(context).colorScheme.primary,
                                ),
                              ),
                            ],
                          ),
                        ),
                        if (_isSelected)
                          Icon(
                            Icons.check_circle,
                            color: Theme.of(context).colorScheme.primary,
                          ),
                      ],
                    ),
                    const SizedBox(height: 16),
                    Text(
                      l10n.premiumPlanDescription,
                      style: Theme.of(context).textTheme.bodyMedium,
                    ),
                    const SizedBox(height: 12),
                    Wrap(
                      spacing: 8,
                      runSpacing: 8,
                      children: [
                        _FeatureTag(label: l10n.featureUnlimited),
                        _FeatureTag(label: l10n.featurePriority),
                        _FeatureTag(label: l10n.featureSupport),
                      ],
                    ),
                  ],
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class _FeatureTag extends StatelessWidget {
  final String label;

  const _FeatureTag({required this.label});

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
      decoration: BoxDecoration(
        color: Theme.of(context).colorScheme.surfaceContainerHighest,
        borderRadius: BorderRadius.circular(20),
      ),
      child: Text(
        label,
        style: Theme.of(context).textTheme.bodySmall,
      ),
    );
  }
}

ARB File Structure for DecoratedBoxTransition

{
  "selectionCardTitle": "Select Plan",
  "@selectionCardTitle": {
    "description": "Title for selection card demo"
  },
  "planCardAccessibility": "{plan} plan, {state}",
  "@planCardAccessibility": {
    "description": "Accessibility label for plan card",
    "placeholders": {
      "plan": {"type": "String"},
      "state": {"type": "String"}
    }
  },
  "selected": "selected",
  "notSelected": "not selected",
  "premiumPlanTitle": "Premium Plan",
  "premiumPlanPrice": "$19.99/month",
  "premiumPlanDescription": "Get access to all premium features including unlimited storage, priority support, and advanced analytics.",
  "featureUnlimited": "Unlimited Storage",
  "featurePriority": "Priority Access",
  "featureSupport": "24/7 Support"
}

Form Validation with Animated Borders

Create form fields with animated validation states:

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

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

  @override
  State<LocalizedValidationForm> createState() => _LocalizedValidationFormState();
}

class _LocalizedValidationFormState extends State<LocalizedValidationForm>
    with TickerProviderStateMixin {
  late AnimationController _emailController;
  late AnimationController _passwordController;
  late Animation<Decoration> _emailDecoration;
  late Animation<Decoration> _passwordDecoration;

  final _emailTextController = TextEditingController();
  final _passwordTextController = TextEditingController();

  _ValidationState _emailState = _ValidationState.neutral;
  _ValidationState _passwordState = _ValidationState.neutral;

  @override
  void initState() {
    super.initState();
    _emailController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 200),
    );
    _passwordController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 200),
    );
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _updateAnimations();
  }

  void _updateAnimations() {
    _emailDecoration = _createDecorationAnimation(_emailController, _emailState);
    _passwordDecoration = _createDecorationAnimation(_passwordController, _passwordState);
  }

  Animation<Decoration> _createDecorationAnimation(
    AnimationController controller,
    _ValidationState state,
  ) {
    final colorScheme = Theme.of(context).colorScheme;

    Color borderColor;
    Color backgroundColor;
    double shadowOpacity;

    switch (state) {
      case _ValidationState.neutral:
        borderColor = colorScheme.outline;
        backgroundColor = colorScheme.surface;
        shadowOpacity = 0.0;
        break;
      case _ValidationState.valid:
        borderColor = Colors.green;
        backgroundColor = Colors.green.withOpacity(0.05);
        shadowOpacity = 0.2;
        break;
      case _ValidationState.invalid:
        borderColor = colorScheme.error;
        backgroundColor = colorScheme.error.withOpacity(0.05);
        shadowOpacity = 0.2;
        break;
    }

    return DecorationTween(
      begin: BoxDecoration(
        color: colorScheme.surface,
        borderRadius: BorderRadius.circular(8),
        border: Border.all(color: colorScheme.outline, width: 1),
      ),
      end: BoxDecoration(
        color: backgroundColor,
        borderRadius: BorderRadius.circular(8),
        border: Border.all(color: borderColor, width: 2),
        boxShadow: [
          BoxShadow(
            color: borderColor.withOpacity(shadowOpacity),
            blurRadius: 8,
            offset: const Offset(0, 2),
          ),
        ],
      ),
    ).animate(CurvedAnimation(
      parent: controller,
      curve: Curves.easeOut,
    ));
  }

  void _validateEmail(String value) {
    final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
    setState(() {
      if (value.isEmpty) {
        _emailState = _ValidationState.neutral;
        _emailController.reverse();
      } else if (emailRegex.hasMatch(value)) {
        _emailState = _ValidationState.valid;
        _updateAnimations();
        _emailController.forward();
      } else {
        _emailState = _ValidationState.invalid;
        _updateAnimations();
        _emailController.forward();
      }
    });
  }

  void _validatePassword(String value) {
    setState(() {
      if (value.isEmpty) {
        _passwordState = _ValidationState.neutral;
        _passwordController.reverse();
      } else if (value.length >= 8) {
        _passwordState = _ValidationState.valid;
        _updateAnimations();
        _passwordController.forward();
      } else {
        _passwordState = _ValidationState.invalid;
        _updateAnimations();
        _passwordController.forward();
      }
    });
  }

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

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

    return Scaffold(
      appBar: AppBar(title: Text(l10n.validationFormTitle)),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              l10n.loginHeader,
              style: Theme.of(context).textTheme.headlineSmall,
            ),
            const SizedBox(height: 8),
            Text(
              l10n.loginSubheader,
              style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                color: Theme.of(context).colorScheme.onSurfaceVariant,
              ),
            ),
            const SizedBox(height: 32),

            // Email field
            Text(
              l10n.emailLabel,
              style: Theme.of(context).textTheme.bodyMedium,
            ),
            const SizedBox(height: 8),
            DecoratedBoxTransition(
              decoration: _emailDecoration,
              child: Semantics(
                textField: true,
                label: l10n.emailFieldAccessibility(_getValidationLabel(l10n, _emailState)),
                child: TextField(
                  controller: _emailTextController,
                  onChanged: _validateEmail,
                  keyboardType: TextInputType.emailAddress,
                  decoration: InputDecoration(
                    hintText: l10n.emailHint,
                    border: InputBorder.none,
                    contentPadding: const EdgeInsets.all(16),
                    suffixIcon: _emailState != _ValidationState.neutral
                        ? Icon(
                            _emailState == _ValidationState.valid
                                ? Icons.check_circle
                                : Icons.error,
                            color: _emailState == _ValidationState.valid
                                ? Colors.green
                                : Theme.of(context).colorScheme.error,
                          )
                        : null,
                  ),
                ),
              ),
            ),
            if (_emailState == _ValidationState.invalid)
              Padding(
                padding: const EdgeInsets.only(top: 8),
                child: Text(
                  l10n.emailInvalidError,
                  style: TextStyle(
                    color: Theme.of(context).colorScheme.error,
                    fontSize: 12,
                  ),
                ),
              ),

            const SizedBox(height: 24),

            // Password field
            Text(
              l10n.passwordLabel,
              style: Theme.of(context).textTheme.bodyMedium,
            ),
            const SizedBox(height: 8),
            DecoratedBoxTransition(
              decoration: _passwordDecoration,
              child: Semantics(
                textField: true,
                label: l10n.passwordFieldAccessibility(_getValidationLabel(l10n, _passwordState)),
                child: TextField(
                  controller: _passwordTextController,
                  onChanged: _validatePassword,
                  obscureText: true,
                  decoration: InputDecoration(
                    hintText: l10n.passwordHint,
                    border: InputBorder.none,
                    contentPadding: const EdgeInsets.all(16),
                    suffixIcon: _passwordState != _ValidationState.neutral
                        ? Icon(
                            _passwordState == _ValidationState.valid
                                ? Icons.check_circle
                                : Icons.error,
                            color: _passwordState == _ValidationState.valid
                                ? Colors.green
                                : Theme.of(context).colorScheme.error,
                          )
                        : null,
                  ),
                ),
              ),
            ),
            if (_passwordState == _ValidationState.invalid)
              Padding(
                padding: const EdgeInsets.only(top: 8),
                child: Text(
                  l10n.passwordTooShortError,
                  style: TextStyle(
                    color: Theme.of(context).colorScheme.error,
                    fontSize: 12,
                  ),
                ),
              ),

            const SizedBox(height: 32),

            SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: (_emailState == _ValidationState.valid &&
                        _passwordState == _ValidationState.valid)
                    ? () {
                        ScaffoldMessenger.of(context).showSnackBar(
                          SnackBar(content: Text(l10n.loginSuccessMessage)),
                        );
                      }
                    : null,
                child: Text(l10n.loginButton),
              ),
            ),
          ],
        ),
      ),
    );
  }

  String _getValidationLabel(AppLocalizations l10n, _ValidationState state) {
    switch (state) {
      case _ValidationState.neutral:
        return l10n.validationNeutral;
      case _ValidationState.valid:
        return l10n.validationValid;
      case _ValidationState.invalid:
        return l10n.validationInvalid;
    }
  }
}

enum _ValidationState { neutral, valid, invalid }

Status Progress Cards

Create progress cards with animated status changes:

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

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

  @override
  State<LocalizedStatusCards> createState() => _LocalizedStatusCardsState();
}

class _LocalizedStatusCardsState extends State<LocalizedStatusCards>
    with TickerProviderStateMixin {
  late List<AnimationController> _controllers;
  late List<_TaskStatus> _statuses;

  @override
  void initState() {
    super.initState();
    _controllers = List.generate(
      4,
      (index) => AnimationController(
        vsync: this,
        duration: const Duration(milliseconds: 300),
      ),
    );
    _statuses = [
      _TaskStatus.completed,
      _TaskStatus.inProgress,
      _TaskStatus.pending,
      _TaskStatus.pending,
    ];

    // Start animations for completed and in-progress items
    for (int i = 0; i < _statuses.length; i++) {
      if (_statuses[i] != _TaskStatus.pending) {
        _controllers[i].forward();
      }
    }
  }

  void _updateStatus(int index) {
    setState(() {
      final currentStatus = _statuses[index];
      switch (currentStatus) {
        case _TaskStatus.pending:
          _statuses[index] = _TaskStatus.inProgress;
          break;
        case _TaskStatus.inProgress:
          _statuses[index] = _TaskStatus.completed;
          break;
        case _TaskStatus.completed:
          _statuses[index] = _TaskStatus.pending;
          break;
      }
      if (_statuses[index] == _TaskStatus.pending) {
        _controllers[index].reverse();
      } else {
        _controllers[index].forward();
      }
    });
  }

  @override
  void dispose() {
    for (final controller in _controllers) {
      controller.dispose();
    }
    super.dispose();
  }

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

    final tasks = [
      _Task(
        icon: Icons.design_services,
        title: l10n.taskDesignTitle,
        description: l10n.taskDesignDescription,
      ),
      _Task(
        icon: Icons.code,
        title: l10n.taskDevelopTitle,
        description: l10n.taskDevelopDescription,
      ),
      _Task(
        icon: Icons.bug_report,
        title: l10n.taskTestTitle,
        description: l10n.taskTestDescription,
      ),
      _Task(
        icon: Icons.rocket_launch,
        title: l10n.taskDeployTitle,
        description: l10n.taskDeployDescription,
      ),
    ];

    return Scaffold(
      appBar: AppBar(title: Text(l10n.statusCardsTitle)),
      body: ListView.builder(
        padding: const EdgeInsets.all(16),
        itemCount: tasks.length,
        itemBuilder: (context, index) {
          final task = tasks[index];
          final status = _statuses[index];

          return Padding(
            padding: const EdgeInsets.only(bottom: 16),
            child: _StatusCard(
              task: task,
              status: status,
              controller: _controllers[index],
              onTap: () => _updateStatus(index),
              l10n: l10n,
            ),
          );
        },
      ),
    );
  }
}

class _StatusCard extends StatelessWidget {
  final _Task task;
  final _TaskStatus status;
  final AnimationController controller;
  final VoidCallback onTap;
  final AppLocalizations l10n;

  const _StatusCard({
    required this.task,
    required this.status,
    required this.controller,
    required this.onTap,
    required this.l10n,
  });

  @override
  Widget build(BuildContext context) {
    final decoration = _createDecoration(context);

    return Semantics(
      button: true,
      label: l10n.taskCardAccessibility(
        task.title,
        _getStatusLabel(),
      ),
      child: GestureDetector(
        onTap: onTap,
        child: DecoratedBoxTransition(
          decoration: decoration,
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: Row(
              children: [
                _buildStatusIndicator(context),
                const SizedBox(width: 16),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        task.title,
                        style: Theme.of(context).textTheme.titleMedium?.copyWith(
                          decoration: status == _TaskStatus.completed
                              ? TextDecoration.lineThrough
                              : null,
                        ),
                      ),
                      const SizedBox(height: 4),
                      Text(
                        task.description,
                        style: Theme.of(context).textTheme.bodySmall?.copyWith(
                          color: Theme.of(context).colorScheme.onSurfaceVariant,
                        ),
                      ),
                    ],
                  ),
                ),
                Container(
                  padding: const EdgeInsets.symmetric(
                    horizontal: 12,
                    vertical: 6,
                  ),
                  decoration: BoxDecoration(
                    color: _getStatusColor(context).withOpacity(0.1),
                    borderRadius: BorderRadius.circular(20),
                  ),
                  child: Text(
                    _getStatusLabel(),
                    style: TextStyle(
                      color: _getStatusColor(context),
                      fontSize: 12,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }

  Widget _buildStatusIndicator(BuildContext context) {
    final color = _getStatusColor(context);
    IconData icon;

    switch (status) {
      case _TaskStatus.pending:
        icon = Icons.radio_button_unchecked;
        break;
      case _TaskStatus.inProgress:
        icon = Icons.sync;
        break;
      case _TaskStatus.completed:
        icon = Icons.check_circle;
        break;
    }

    return Container(
      width: 48,
      height: 48,
      decoration: BoxDecoration(
        color: color.withOpacity(0.1),
        borderRadius: BorderRadius.circular(12),
      ),
      child: Icon(icon, color: color),
    );
  }

  Animation<Decoration> _createDecoration(BuildContext context) {
    final colorScheme = Theme.of(context).colorScheme;
    final statusColor = _getStatusColor(context);

    return DecorationTween(
      begin: BoxDecoration(
        color: colorScheme.surface,
        borderRadius: BorderRadius.circular(12),
        border: Border.all(
          color: colorScheme.outline.withOpacity(0.3),
          width: 1,
        ),
      ),
      end: BoxDecoration(
        color: statusColor.withOpacity(0.05),
        borderRadius: BorderRadius.circular(12),
        border: Border.all(
          color: statusColor.withOpacity(0.5),
          width: 2,
        ),
        boxShadow: [
          BoxShadow(
            color: statusColor.withOpacity(0.1),
            blurRadius: 8,
            offset: const Offset(0, 2),
          ),
        ],
      ),
    ).animate(CurvedAnimation(
      parent: controller,
      curve: Curves.easeOut,
    ));
  }

  Color _getStatusColor(BuildContext context) {
    switch (status) {
      case _TaskStatus.pending:
        return Theme.of(context).colorScheme.outline;
      case _TaskStatus.inProgress:
        return Colors.orange;
      case _TaskStatus.completed:
        return Colors.green;
    }
  }

  String _getStatusLabel() {
    switch (status) {
      case _TaskStatus.pending:
        return l10n.statusPending;
      case _TaskStatus.inProgress:
        return l10n.statusInProgress;
      case _TaskStatus.completed:
        return l10n.statusCompleted;
    }
  }
}

class _Task {
  final IconData icon;
  final String title;
  final String description;

  _Task({
    required this.icon,
    required this.title,
    required this.description,
  });
}

enum _TaskStatus { pending, inProgress, completed }

Theme-Aware Card Animation

Create cards that animate decoration based on theme:

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

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

  @override
  State<LocalizedThemeCard> createState() => _LocalizedThemeCardState();
}

class _LocalizedThemeCardState extends State<LocalizedThemeCard>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  bool _isDarkMode = false;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 400),
    );
  }

  void _toggleTheme() {
    setState(() {
      _isDarkMode = !_isDarkMode;
      if (_isDarkMode) {
        _controller.forward();
      } else {
        _controller.reverse();
      }
    });
  }

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

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

    final lightDecoration = BoxDecoration(
      gradient: const LinearGradient(
        colors: [Color(0xFFF5F5F5), Colors.white],
        begin: Alignment.topLeft,
        end: Alignment.bottomRight,
      ),
      borderRadius: BorderRadius.circular(16),
      border: Border.all(color: Colors.grey.shade300),
      boxShadow: [
        BoxShadow(
          color: Colors.black.withOpacity(0.08),
          blurRadius: 12,
          offset: const Offset(0, 4),
        ),
      ],
    );

    final darkDecoration = BoxDecoration(
      gradient: const LinearGradient(
        colors: [Color(0xFF2D2D2D), Color(0xFF1A1A1A)],
        begin: Alignment.topLeft,
        end: Alignment.bottomRight,
      ),
      borderRadius: BorderRadius.circular(16),
      border: Border.all(color: Colors.grey.shade800),
      boxShadow: [
        BoxShadow(
          color: Colors.black.withOpacity(0.4),
          blurRadius: 16,
          offset: const Offset(0, 6),
        ),
      ],
    );

    final decorationAnimation = DecorationTween(
      begin: lightDecoration,
      end: darkDecoration,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeInOut,
    ));

    return Scaffold(
      appBar: AppBar(title: Text(l10n.themeCardTitle)),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            Semantics(
              container: true,
              label: l10n.themeCardAccessibility(_isDarkMode ? l10n.darkTheme : l10n.lightTheme),
              child: DecoratedBoxTransition(
                decoration: decorationAnimation,
                child: Padding(
                  padding: const EdgeInsets.all(24),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Row(
                        children: [
                          AnimatedBuilder(
                            animation: _controller,
                            builder: (context, child) {
                              return Icon(
                                _isDarkMode ? Icons.dark_mode : Icons.light_mode,
                                size: 32,
                                color: _isDarkMode ? Colors.amber : Colors.orange,
                              );
                            },
                          ),
                          const SizedBox(width: 12),
                          Text(
                            _isDarkMode ? l10n.darkTheme : l10n.lightTheme,
                            style: TextStyle(
                              fontSize: 24,
                              fontWeight: FontWeight.bold,
                              color: _isDarkMode ? Colors.white : Colors.black87,
                            ),
                          ),
                        ],
                      ),
                      const SizedBox(height: 16),
                      Text(
                        l10n.themeDescription,
                        style: TextStyle(
                          fontSize: 16,
                          color: _isDarkMode ? Colors.grey.shade300 : Colors.grey.shade700,
                        ),
                      ),
                      const SizedBox(height: 24),
                      Row(
                        children: [
                          _ThemeFeature(
                            icon: Icons.visibility,
                            label: l10n.featureContrast,
                            isDark: _isDarkMode,
                          ),
                          const SizedBox(width: 16),
                          _ThemeFeature(
                            icon: Icons.battery_full,
                            label: l10n.featureBattery,
                            isDark: _isDarkMode,
                          ),
                          const SizedBox(width: 16),
                          _ThemeFeature(
                            icon: Icons.bedtime,
                            label: l10n.featureComfort,
                            isDark: _isDarkMode,
                          ),
                        ],
                      ),
                    ],
                  ),
                ),
              ),
            ),
            const SizedBox(height: 32),
            ElevatedButton.icon(
              onPressed: _toggleTheme,
              icon: Icon(_isDarkMode ? Icons.light_mode : Icons.dark_mode),
              label: Text(_isDarkMode ? l10n.switchToLight : l10n.switchToDark),
            ),
          ],
        ),
      ),
    );
  }
}

class _ThemeFeature extends StatelessWidget {
  final IconData icon;
  final String label;
  final bool isDark;

  const _ThemeFeature({
    required this.icon,
    required this.label,
    required this.isDark,
  });

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Icon(
          icon,
          color: isDark ? Colors.grey.shade400 : Colors.grey.shade600,
        ),
        const SizedBox(height: 4),
        Text(
          label,
          style: TextStyle(
            fontSize: 12,
            color: isDark ? Colors.grey.shade400 : Colors.grey.shade600,
          ),
        ),
      ],
    );
  }
}

Interactive Button States

Create buttons with animated decoration states:

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

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

  @override
  State<LocalizedInteractiveButton> createState() => _LocalizedInteractiveButtonState();
}

class _LocalizedInteractiveButtonState extends State<LocalizedInteractiveButton>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<Decoration> _decorationAnimation;

  bool _isPressed = false;
  bool _isHovered = false;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 150),
    );
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _updateAnimation();
  }

  void _updateAnimation() {
    final colorScheme = Theme.of(context).colorScheme;

    BoxDecoration normalDecoration = BoxDecoration(
      gradient: LinearGradient(
        colors: [
          colorScheme.primary,
          colorScheme.primary.withBlue(colorScheme.primary.blue + 30),
        ],
        begin: Alignment.topLeft,
        end: Alignment.bottomRight,
      ),
      borderRadius: BorderRadius.circular(12),
      boxShadow: [
        BoxShadow(
          color: colorScheme.primary.withOpacity(0.3),
          blurRadius: 8,
          offset: const Offset(0, 4),
        ),
      ],
    );

    BoxDecoration pressedDecoration = BoxDecoration(
      gradient: LinearGradient(
        colors: [
          colorScheme.primary.withOpacity(0.8),
          colorScheme.primary.withBlue(colorScheme.primary.blue + 30).withOpacity(0.8),
        ],
        begin: Alignment.topLeft,
        end: Alignment.bottomRight,
      ),
      borderRadius: BorderRadius.circular(12),
      boxShadow: [
        BoxShadow(
          color: colorScheme.primary.withOpacity(0.2),
          blurRadius: 4,
          offset: const Offset(0, 2),
        ),
      ],
    );

    _decorationAnimation = DecorationTween(
      begin: normalDecoration,
      end: pressedDecoration,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeOut,
    ));
  }

  void _onTapDown(TapDownDetails details) {
    setState(() => _isPressed = true);
    _controller.forward();
  }

  void _onTapUp(TapUpDetails details) {
    setState(() => _isPressed = false);
    _controller.reverse();
  }

  void _onTapCancel() {
    setState(() => _isPressed = false);
    _controller.reverse();
  }

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

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

    return Scaffold(
      appBar: AppBar(title: Text(l10n.interactiveButtonTitle)),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text(
              l10n.buttonStateLabel(_isPressed ? l10n.pressed : l10n.normal),
              style: Theme.of(context).textTheme.bodyMedium,
            ),
            const SizedBox(height: 24),
            Semantics(
              button: true,
              label: l10n.primaryActionButton,
              child: GestureDetector(
                onTapDown: _onTapDown,
                onTapUp: _onTapUp,
                onTapCancel: _onTapCancel,
                onTap: () {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(content: Text(l10n.buttonTapped)),
                  );
                },
                child: DecoratedBoxTransition(
                  decoration: _decorationAnimation,
                  child: Container(
                    padding: const EdgeInsets.symmetric(
                      horizontal: 48,
                      vertical: 16,
                    ),
                    child: Row(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        Icon(
                          Icons.send,
                          color: Theme.of(context).colorScheme.onPrimary,
                        ),
                        const SizedBox(width: 12),
                        Text(
                          l10n.submitButtonText,
                          style: TextStyle(
                            color: Theme.of(context).colorScheme.onPrimary,
                            fontSize: 18,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ),
            ),
            const SizedBox(height: 48),
            Text(
              l10n.buttonInstructions,
              style: Theme.of(context).textTheme.bodySmall?.copyWith(
                color: Theme.of(context).colorScheme.onSurfaceVariant,
              ),
              textAlign: TextAlign.center,
            ),
          ],
        ),
      ),
    );
  }
}

Complete ARB File for DecoratedBoxTransition

{
  "@@locale": "en",

  "selectionCardTitle": "Select Plan",
  "planCardAccessibility": "{plan} plan, {state}",
  "@planCardAccessibility": {
    "placeholders": {
      "plan": {"type": "String"},
      "state": {"type": "String"}
    }
  },
  "selected": "selected",
  "notSelected": "not selected",
  "premiumPlanTitle": "Premium Plan",
  "premiumPlanPrice": "$19.99/month",
  "premiumPlanDescription": "Get access to all premium features including unlimited storage, priority support, and advanced analytics.",
  "featureUnlimited": "Unlimited Storage",
  "featurePriority": "Priority Access",
  "featureSupport": "24/7 Support",

  "validationFormTitle": "Login Form",
  "loginHeader": "Welcome Back",
  "loginSubheader": "Enter your credentials to continue",
  "emailLabel": "Email Address",
  "emailHint": "you@example.com",
  "emailFieldAccessibility": "Email field, {state}",
  "@emailFieldAccessibility": {
    "placeholders": {
      "state": {"type": "String"}
    }
  },
  "emailInvalidError": "Please enter a valid email address",
  "passwordLabel": "Password",
  "passwordHint": "Enter your password",
  "passwordFieldAccessibility": "Password field, {state}",
  "@passwordFieldAccessibility": {
    "placeholders": {
      "state": {"type": "String"}
    }
  },
  "passwordTooShortError": "Password must be at least 8 characters",
  "loginButton": "Sign In",
  "loginSuccessMessage": "Login successful!",
  "validationNeutral": "empty",
  "validationValid": "valid",
  "validationInvalid": "invalid",

  "statusCardsTitle": "Project Status",
  "taskDesignTitle": "Design Phase",
  "taskDesignDescription": "Create UI mockups and wireframes",
  "taskDevelopTitle": "Development",
  "taskDevelopDescription": "Implement features and functionality",
  "taskTestTitle": "Testing",
  "taskTestDescription": "Quality assurance and bug fixes",
  "taskDeployTitle": "Deployment",
  "taskDeployDescription": "Release to production",
  "taskCardAccessibility": "{title}, status: {status}",
  "@taskCardAccessibility": {
    "placeholders": {
      "title": {"type": "String"},
      "status": {"type": "String"}
    }
  },
  "statusPending": "Pending",
  "statusInProgress": "In Progress",
  "statusCompleted": "Completed",

  "themeCardTitle": "Theme Demo",
  "darkTheme": "Dark Theme",
  "lightTheme": "Light Theme",
  "themeDescription": "Experience the smooth transition between light and dark themes with animated decorations.",
  "featureContrast": "Contrast",
  "featureBattery": "Battery",
  "featureComfort": "Comfort",
  "switchToLight": "Switch to Light",
  "switchToDark": "Switch to Dark",
  "themeCardAccessibility": "Theme preview card, currently {theme}",
  "@themeCardAccessibility": {
    "placeholders": {
      "theme": {"type": "String"}
    }
  },

  "interactiveButtonTitle": "Interactive Button",
  "buttonStateLabel": "Current state: {state}",
  "@buttonStateLabel": {
    "placeholders": {
      "state": {"type": "String"}
    }
  },
  "pressed": "Pressed",
  "normal": "Normal",
  "primaryActionButton": "Submit action button",
  "submitButtonText": "Submit",
  "buttonTapped": "Button tapped!",
  "buttonInstructions": "Press and hold to see the decoration animation"
}

Best Practices Summary

  1. Use DecorationTween correctly: DecoratedBoxTransition requires Animation
  2. Handle color scheme changes: Update animations when theme changes
  3. Dispose controllers properly: Always dispose AnimationControllers in the dispose method
  4. Add accessibility information: Use Semantics to describe decoration state changes
  5. Keep animations subtle: Decoration changes should enhance, not distract
  6. Use appropriate curves: easeInOut works well for most decoration transitions
  7. Consider color contrast: Ensure animated colors maintain accessibility standards
  8. Test across themes: Verify animations work in both light and dark modes
  9. Avoid complex gradients: Simple decorations animate more smoothly
  10. Combine with other animations: DecoratedBoxTransition pairs well with ScaleTransition and FadeTransition

Conclusion

DecoratedBoxTransition provides precise control over decoration animations in Flutter apps. By using explicit AnimationControllers with DecorationTween, you can create polished selection states, validation feedback, status indicators, and theme transitions that enhance the user experience across all languages. The key is combining proper accessibility announcements with smooth, well-timed decoration changes that feel natural and purposeful.

Remember to always dispose of your AnimationControllers and test your decoration animations across different themes and screen sizes to ensure they work correctly and maintain proper color contrast for accessibility.