Flutter AnimatedOpacity Localization: Fade Transitions, Visibility States, and Accessible Announcements
AnimatedOpacity provides smooth fade-in and fade-out animations for widgets. Proper localization ensures visibility transitions, loading states, and conditional content work seamlessly across languages with proper accessibility announcements. This guide covers comprehensive strategies for localizing AnimatedOpacity widgets in Flutter.
Understanding AnimatedOpacity Localization
AnimatedOpacity widgets require localization for:
- Visibility announcements: Screen reader notifications when content appears/disappears
- Loading state messages: Fade transitions during data loading
- Conditional content: Showing/hiding elements based on state
- Error state transitions: Fading error messages in and out
- Hint text animations: Fading helper text and tooltips
- Progressive disclosure: Revealing content step by step
Basic AnimatedOpacity with Accessibility
Start with a simple fade transition with proper accessibility:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedFadeContent extends StatefulWidget {
final bool isVisible;
final Widget child;
final String visibleLabel;
final String hiddenLabel;
const LocalizedFadeContent({
super.key,
required this.isVisible,
required this.child,
required this.visibleLabel,
required this.hiddenLabel,
});
@override
State<LocalizedFadeContent> createState() => _LocalizedFadeContentState();
}
class _LocalizedFadeContentState extends State<LocalizedFadeContent> {
@override
void didUpdateWidget(LocalizedFadeContent oldWidget) {
super.didUpdateWidget(oldWidget);
// Announce visibility changes to screen readers
if (oldWidget.isVisible != widget.isVisible) {
final l10n = AppLocalizations.of(context)!;
SemanticsService.announce(
widget.isVisible ? widget.visibleLabel : widget.hiddenLabel,
Directionality.of(context),
);
}
}
@override
Widget build(BuildContext context) {
return Semantics(
hidden: !widget.isVisible,
child: AnimatedOpacity(
opacity: widget.isVisible ? 1.0 : 0.0,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: IgnorePointer(
ignoring: !widget.isVisible,
child: widget.child,
),
),
);
}
}
// Usage example
class FeatureHighlight extends StatefulWidget {
const FeatureHighlight({super.key});
@override
State<FeatureHighlight> createState() => _FeatureHighlightState();
}
class _FeatureHighlightState extends State<FeatureHighlight> {
bool _showDetails = false;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Column(
children: [
ListTile(
title: Text(l10n.featureTitle),
trailing: TextButton(
onPressed: () => setState(() => _showDetails = !_showDetails),
child: Text(_showDetails ? l10n.hideDetails : l10n.showDetails),
),
),
LocalizedFadeContent(
isVisible: _showDetails,
visibleLabel: l10n.detailsShown,
hiddenLabel: l10n.detailsHidden,
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(l10n.featureDescription),
),
),
],
);
}
}
ARB File Structure for AnimatedOpacity
{
"featureTitle": "Premium Feature",
"@featureTitle": {
"description": "Title of the feature section"
},
"featureDescription": "This premium feature includes advanced analytics, custom reports, and priority support. Upgrade your plan to unlock all capabilities.",
"@featureDescription": {
"description": "Detailed feature description"
},
"showDetails": "Show details",
"@showDetails": {
"description": "Button to show details"
},
"hideDetails": "Hide details",
"@hideDetails": {
"description": "Button to hide details"
},
"detailsShown": "Details section is now visible",
"@detailsShown": {
"description": "Announcement when details become visible"
},
"detailsHidden": "Details section is now hidden",
"@detailsHidden": {
"description": "Announcement when details are hidden"
}
}
Loading State with Fade Transition
Create a loading overlay with localized messages:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedLoadingOverlay extends StatelessWidget {
final bool isLoading;
final String? loadingMessage;
final Widget child;
const LocalizedLoadingOverlay({
super.key,
required this.isLoading,
this.loadingMessage,
required this.child,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Stack(
children: [
// Main content with reduced opacity when loading
AnimatedOpacity(
opacity: isLoading ? 0.5 : 1.0,
duration: const Duration(milliseconds: 200),
child: IgnorePointer(
ignoring: isLoading,
child: child,
),
),
// Loading overlay
AnimatedOpacity(
opacity: isLoading ? 1.0 : 0.0,
duration: const Duration(milliseconds: 200),
child: IgnorePointer(
ignoring: !isLoading,
child: Container(
color: Colors.transparent,
child: Center(
child: Semantics(
liveRegion: true,
label: loadingMessage ?? l10n.loading,
child: Card(
elevation: 8,
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text(
loadingMessage ?? l10n.loading,
style: Theme.of(context).textTheme.bodyLarge,
),
],
),
),
),
),
),
),
),
),
],
);
}
}
// Multi-step loading with progress
class LocalizedStepLoadingOverlay extends StatelessWidget {
final bool isLoading;
final int currentStep;
final int totalSteps;
final String currentStepLabel;
final Widget child;
const LocalizedStepLoadingOverlay({
super.key,
required this.isLoading,
required this.currentStep,
required this.totalSteps,
required this.currentStepLabel,
required this.child,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Stack(
children: [
AnimatedOpacity(
opacity: isLoading ? 0.3 : 1.0,
duration: const Duration(milliseconds: 300),
child: IgnorePointer(
ignoring: isLoading,
child: child,
),
),
AnimatedOpacity(
opacity: isLoading ? 1.0 : 0.0,
duration: const Duration(milliseconds: 300),
child: IgnorePointer(
ignoring: !isLoading,
child: Center(
child: Semantics(
liveRegion: true,
label: l10n.loadingStepProgress(
currentStep,
totalSteps,
currentStepLabel,
),
child: Card(
elevation: 12,
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Stack(
alignment: Alignment.center,
children: [
SizedBox(
width: 80,
height: 80,
child: CircularProgressIndicator(
value: currentStep / totalSteps,
strokeWidth: 6,
),
),
Text(
l10n.stepCounter(currentStep, totalSteps),
style: Theme.of(context).textTheme.titleMedium,
),
],
),
const SizedBox(height: 20),
Text(
currentStepLabel,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
l10n.pleaseWait,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.outline,
),
),
],
),
),
),
),
),
),
),
],
);
}
}
Form Validation with Fading Messages
Create form fields with animated error messages:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedValidatedField extends StatefulWidget {
final String label;
final String? errorMessage;
final String? helperText;
final TextEditingController controller;
final String? Function(String?)? validator;
final TextInputType? keyboardType;
const LocalizedValidatedField({
super.key,
required this.label,
this.errorMessage,
this.helperText,
required this.controller,
this.validator,
this.keyboardType,
});
@override
State<LocalizedValidatedField> createState() => _LocalizedValidatedFieldState();
}
class _LocalizedValidatedFieldState extends State<LocalizedValidatedField> {
String? _currentError;
bool _hasInteracted = false;
@override
void initState() {
super.initState();
widget.controller.addListener(_validateOnChange);
}
void _validateOnChange() {
if (!_hasInteracted) return;
final error = widget.validator?.call(widget.controller.text);
if (error != _currentError) {
setState(() => _currentError = error);
// Announce error to screen readers
if (error != null) {
SemanticsService.announce(
error,
Directionality.of(context),
);
}
}
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final hasError = _currentError != null;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: widget.controller,
keyboardType: widget.keyboardType,
decoration: InputDecoration(
labelText: widget.label,
helperText: widget.helperText,
errorText: _currentError,
border: const OutlineInputBorder(),
suffixIcon: AnimatedOpacity(
opacity: hasError ? 1.0 : 0.0,
duration: const Duration(milliseconds: 200),
child: Icon(
Icons.error_outline,
color: Theme.of(context).colorScheme.error,
semanticLabel: l10n.fieldHasError,
),
),
),
onTap: () {
if (!_hasInteracted) {
setState(() => _hasInteracted = true);
}
},
),
// Animated helper message
AnimatedOpacity(
opacity: !hasError && widget.helperText != null ? 1.0 : 0.0,
duration: const Duration(milliseconds: 200),
child: Padding(
padding: const EdgeInsets.only(top: 4, left: 12),
child: Text(
widget.helperText ?? '',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.outline,
),
),
),
),
],
);
}
@override
void dispose() {
widget.controller.removeListener(_validateOnChange);
super.dispose();
}
}
// Example form with validation
class LocalizedRegistrationForm extends StatefulWidget {
const LocalizedRegistrationForm({super.key});
@override
State<LocalizedRegistrationForm> createState() =>
_LocalizedRegistrationFormState();
}
class _LocalizedRegistrationFormState extends State<LocalizedRegistrationForm> {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _showPasswordRequirements = false;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
LocalizedValidatedField(
label: l10n.emailLabel,
helperText: l10n.emailHelperText,
controller: _emailController,
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.isEmpty) {
return l10n.emailRequired;
}
if (!value.contains('@')) {
return l10n.emailInvalid;
}
return null;
},
),
const SizedBox(height: 16),
LocalizedValidatedField(
label: l10n.passwordLabel,
controller: _passwordController,
validator: (value) {
if (value == null || value.isEmpty) {
return l10n.passwordRequired;
}
if (value.length < 8) {
return l10n.passwordTooShort;
}
return null;
},
),
const SizedBox(height: 8),
// Fading password requirements
Focus(
onFocusChange: (hasFocus) {
setState(() => _showPasswordRequirements = hasFocus);
},
child: AnimatedOpacity(
opacity: _showPasswordRequirements ? 1.0 : 0.0,
duration: const Duration(milliseconds: 200),
child: Semantics(
label: l10n.passwordRequirementsLabel,
child: Card(
color: Theme.of(context).colorScheme.surfaceVariant,
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.passwordRequirements,
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 8),
_buildRequirement(
l10n.passwordMinLength,
_passwordController.text.length >= 8,
),
_buildRequirement(
l10n.passwordHasNumber,
_passwordController.text.contains(RegExp(r'[0-9]')),
),
_buildRequirement(
l10n.passwordHasUppercase,
_passwordController.text.contains(RegExp(r'[A-Z]')),
),
],
),
),
),
),
),
),
],
),
);
}
Widget _buildRequirement(String text, bool isMet) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
children: [
Icon(
isMet ? Icons.check_circle : Icons.circle_outlined,
size: 16,
color: isMet ? Colors.green : Colors.grey,
),
const SizedBox(width: 8),
Text(
text,
style: TextStyle(
color: isMet ? Colors.green.shade700 : Colors.grey.shade600,
decoration: isMet ? TextDecoration.lineThrough : null,
),
),
],
),
);
}
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
}
Staggered Fade Animation for Lists
Create lists with staggered fade-in effects:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedStaggeredFadeList extends StatefulWidget {
final List<ListItemData> items;
const LocalizedStaggeredFadeList({super.key, required this.items});
@override
State<LocalizedStaggeredFadeList> createState() =>
_LocalizedStaggeredFadeListState();
}
class _LocalizedStaggeredFadeListState
extends State<LocalizedStaggeredFadeList> {
final List<bool> _visibleItems = [];
@override
void initState() {
super.initState();
_visibleItems.addAll(List.filled(widget.items.length, false));
_animateItems();
}
void _animateItems() async {
for (int i = 0; i < widget.items.length; i++) {
await Future.delayed(const Duration(milliseconds: 100));
if (mounted) {
setState(() => _visibleItems[i] = true);
}
}
// Announce completion to screen readers
if (mounted) {
final l10n = AppLocalizations.of(context)!;
SemanticsService.announce(
l10n.listLoadedItems(widget.items.length),
Directionality.of(context),
);
}
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isRtl = Directionality.of(context) == TextDirection.rtl;
return ListView.builder(
itemCount: widget.items.length,
itemBuilder: (context, index) {
final item = widget.items[index];
final isVisible = _visibleItems[index];
return AnimatedOpacity(
opacity: isVisible ? 1.0 : 0.0,
duration: const Duration(milliseconds: 400),
curve: Curves.easeOut,
child: AnimatedSlide(
offset: isVisible ? Offset.zero : Offset(isRtl ? -0.1 : 0.1, 0),
duration: const Duration(milliseconds: 400),
curve: Curves.easeOut,
child: Semantics(
label: l10n.listItemPosition(index + 1, widget.items.length),
child: ListTile(
leading: CircleAvatar(
child: Text(item.initials),
),
title: Text(item.title),
subtitle: Text(item.subtitle),
trailing: Icon(
Icons.chevron_right,
semanticLabel: l10n.viewDetails,
),
),
),
),
);
},
);
}
}
class ListItemData {
final String title;
final String subtitle;
final String initials;
ListItemData({
required this.title,
required this.subtitle,
required this.initials,
});
}
Conditional Messaging with Fade Transitions
Handle various states with fading messages:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
enum ContentState { loading, empty, error, success }
class LocalizedStateContent extends StatelessWidget {
final ContentState state;
final Widget? successContent;
final String? errorMessage;
final VoidCallback? onRetry;
const LocalizedStateContent({
super.key,
required this.state,
this.successContent,
this.errorMessage,
this.onRetry,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Stack(
children: [
// Loading state
_FadingStateWidget(
isVisible: state == ContentState.loading,
child: _buildLoadingState(l10n),
),
// Empty state
_FadingStateWidget(
isVisible: state == ContentState.empty,
child: _buildEmptyState(context, l10n),
),
// Error state
_FadingStateWidget(
isVisible: state == ContentState.error,
child: _buildErrorState(context, l10n),
),
// Success state
_FadingStateWidget(
isVisible: state == ContentState.success,
child: successContent ?? const SizedBox.shrink(),
),
],
);
}
Widget _buildLoadingState(AppLocalizations l10n) {
return Center(
child: Semantics(
liveRegion: true,
label: l10n.contentLoading,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text(l10n.loading),
],
),
),
);
}
Widget _buildEmptyState(BuildContext context, AppLocalizations l10n) {
return Center(
child: Semantics(
liveRegion: true,
label: l10n.noContentAvailable,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.inbox_outlined,
size: 64,
color: Theme.of(context).colorScheme.outline,
),
const SizedBox(height: 16),
Text(
l10n.emptyStateTitle,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Text(
l10n.emptyStateMessage,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.outline,
),
textAlign: TextAlign.center,
),
],
),
),
);
}
Widget _buildErrorState(BuildContext context, AppLocalizations l10n) {
return Center(
child: Semantics(
liveRegion: true,
label: l10n.errorOccurred,
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.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.error,
),
),
const SizedBox(height: 8),
Text(
errorMessage ?? l10n.errorGenericMessage,
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
),
if (onRetry != null) ...[
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: onRetry,
icon: const Icon(Icons.refresh),
label: Text(l10n.retryButton),
),
],
],
),
),
);
}
}
class _FadingStateWidget extends StatelessWidget {
final bool isVisible;
final Widget child;
const _FadingStateWidget({
required this.isVisible,
required this.child,
});
@override
Widget build(BuildContext context) {
return AnimatedOpacity(
opacity: isVisible ? 1.0 : 0.0,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: IgnorePointer(
ignoring: !isVisible,
child: child,
),
);
}
}
Tooltip with Fade Animation
Create accessible tooltips that fade in:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedFadingTooltip extends StatefulWidget {
final Widget child;
final String message;
final TooltipPosition position;
const LocalizedFadingTooltip({
super.key,
required this.child,
required this.message,
this.position = TooltipPosition.bottom,
});
@override
State<LocalizedFadingTooltip> createState() => _LocalizedFadingTooltipState();
}
class _LocalizedFadingTooltipState extends State<LocalizedFadingTooltip> {
bool _isVisible = false;
final _layerLink = LayerLink();
OverlayEntry? _overlayEntry;
void _showTooltip() {
setState(() => _isVisible = true);
_overlayEntry = OverlayEntry(
builder: (context) => _TooltipOverlay(
message: widget.message,
position: widget.position,
layerLink: _layerLink,
isVisible: _isVisible,
),
);
Overlay.of(context).insert(_overlayEntry!);
// Announce to screen readers
SemanticsService.announce(
widget.message,
Directionality.of(context),
);
// Auto-hide after delay
Future.delayed(const Duration(seconds: 3), () {
_hideTooltip();
});
}
void _hideTooltip() {
setState(() => _isVisible = false);
Future.delayed(const Duration(milliseconds: 200), () {
_overlayEntry?.remove();
_overlayEntry = null;
});
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return CompositedTransformTarget(
link: _layerLink,
child: GestureDetector(
onLongPress: _showTooltip,
child: Semantics(
tooltip: widget.message,
label: l10n.longPressForTooltip,
child: widget.child,
),
),
);
}
@override
void dispose() {
_overlayEntry?.remove();
super.dispose();
}
}
class _TooltipOverlay extends StatelessWidget {
final String message;
final TooltipPosition position;
final LayerLink layerLink;
final bool isVisible;
const _TooltipOverlay({
required this.message,
required this.position,
required this.layerLink,
required this.isVisible,
});
@override
Widget build(BuildContext context) {
final offset = switch (position) {
TooltipPosition.top => const Offset(0, -48),
TooltipPosition.bottom => const Offset(0, 32),
TooltipPosition.left => const Offset(-150, 0),
TooltipPosition.right => const Offset(32, 0),
};
return CompositedTransformFollower(
link: layerLink,
offset: offset,
child: AnimatedOpacity(
opacity: isVisible ? 1.0 : 0.0,
duration: const Duration(milliseconds: 200),
child: Material(
color: Colors.transparent,
child: Container(
constraints: const BoxConstraints(maxWidth: 200),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.grey.shade800,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Text(
message,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
),
textAlign: TextAlign.center,
),
),
),
),
);
}
}
enum TooltipPosition { top, bottom, left, right }
Complete ARB File for AnimatedOpacity
{
"@@locale": "en",
"loading": "Loading...",
"@loading": {
"description": "Generic loading message"
},
"contentLoading": "Content is loading",
"@contentLoading": {
"description": "Accessibility label for loading state"
},
"pleaseWait": "Please wait...",
"@pleaseWait": {
"description": "Please wait message"
},
"loadingStepProgress": "Loading step {current} of {total}: {label}",
"@loadingStepProgress": {
"description": "Step loading progress announcement",
"placeholders": {
"current": {"type": "int"},
"total": {"type": "int"},
"label": {"type": "String"}
}
},
"stepCounter": "{current}/{total}",
"@stepCounter": {
"description": "Step counter display",
"placeholders": {
"current": {"type": "int"},
"total": {"type": "int"}
}
},
"emailLabel": "Email address",
"@emailLabel": {
"description": "Email field label"
},
"emailHelperText": "We'll never share your email",
"@emailHelperText": {
"description": "Email field helper text"
},
"emailRequired": "Email is required",
"@emailRequired": {
"description": "Email required error"
},
"emailInvalid": "Please enter a valid email address",
"@emailInvalid": {
"description": "Invalid email error"
},
"passwordLabel": "Password",
"@passwordLabel": {
"description": "Password field label"
},
"passwordRequired": "Password is required",
"@passwordRequired": {
"description": "Password required error"
},
"passwordTooShort": "Password must be at least 8 characters",
"@passwordTooShort": {
"description": "Password too short error"
},
"fieldHasError": "This field has an error",
"@fieldHasError": {
"description": "Field error accessibility label"
},
"passwordRequirements": "Password requirements:",
"@passwordRequirements": {
"description": "Password requirements title"
},
"passwordRequirementsLabel": "Password requirements checklist",
"@passwordRequirementsLabel": {
"description": "Password requirements accessibility label"
},
"passwordMinLength": "At least 8 characters",
"@passwordMinLength": {
"description": "Minimum length requirement"
},
"passwordHasNumber": "Contains a number",
"@passwordHasNumber": {
"description": "Number requirement"
},
"passwordHasUppercase": "Contains an uppercase letter",
"@passwordHasUppercase": {
"description": "Uppercase requirement"
},
"listLoadedItems": "{count} items loaded",
"@listLoadedItems": {
"description": "List loaded announcement",
"placeholders": {
"count": {"type": "int"}
}
},
"listItemPosition": "Item {position} of {total}",
"@listItemPosition": {
"description": "List item position accessibility label",
"placeholders": {
"position": {"type": "int"},
"total": {"type": "int"}
}
},
"viewDetails": "View details",
"@viewDetails": {
"description": "View details accessibility label"
},
"noContentAvailable": "No content available",
"@noContentAvailable": {
"description": "No content accessibility label"
},
"emptyStateTitle": "Nothing here yet",
"@emptyStateTitle": {
"description": "Empty state title"
},
"emptyStateMessage": "Content will appear here once it's available.",
"@emptyStateMessage": {
"description": "Empty state message"
},
"errorOccurred": "An error occurred",
"@errorOccurred": {
"description": "Error accessibility label"
},
"errorTitle": "Something went wrong",
"@errorTitle": {
"description": "Error title"
},
"errorGenericMessage": "We couldn't load the content. Please try again.",
"@errorGenericMessage": {
"description": "Generic error message"
},
"retryButton": "Try again",
"@retryButton": {
"description": "Retry button label"
},
"longPressForTooltip": "Long press for more information",
"@longPressForTooltip": {
"description": "Tooltip hint accessibility label"
}
}
Best Practices Summary
- Announce visibility changes: Use SemanticsService.announce for screen readers
- Use IgnorePointer with opacity: Prevent interaction when content is hidden
- Set Semantics.hidden: Mark invisible content as hidden for accessibility
- Provide liveRegion for dynamic content: Enable real-time announcements
- Handle staggered animations: Create engaging list entrance effects
- Combine with AnimatedSlide: Add directional movement to fade effects
- Support RTL animations: Adjust slide directions for bidirectional layouts
- Use appropriate durations: 200-400ms for typical fade transitions
- Choose proper curves: easeInOut for smooth bidirectional fades
- Test with screen readers: Verify announcements work correctly
Conclusion
Proper AnimatedOpacity localization ensures smooth, accessible fade transitions across all languages. By announcing visibility changes, marking hidden content appropriately, and providing comprehensive accessibility labels, you create polished fade animations that work well for users worldwide. The patterns shown here—loading overlays, form validation, staggered lists, and state transitions—can be adapted to any Flutter application requiring animated opacity with localization support.
Remember to test your fade animations with screen readers to verify that visibility changes are properly announced for all your supported languages.