Flutter IgnorePointer Localization: Touch Control for Multilingual Interfaces
IgnorePointer is a Flutter widget that prevents its child from receiving pointer events like taps and drags. In multilingual applications, IgnorePointer enables controlled interaction states that communicate effectively across languages through visual cues rather than text alone.
Understanding IgnorePointer in Localization Context
IgnorePointer makes a widget invisible to hit testing, allowing pointer events to pass through to widgets below. For multilingual apps, this enables:
- Disabled state management without relying on translated text
- Overlay patterns that don't block underlying interactions
- Tutorial and onboarding flows that work across languages
- Loading states with universal visual feedback
Why IgnorePointer Matters for Multilingual Apps
IgnorePointer provides:
- Universal interaction control: Disable without language-specific states
- Visual feedback patterns: Combine with opacity for clear disabled states
- Overlay flexibility: Create non-blocking overlays in any language
- Consistent UX: Same interaction behavior across all locales
Basic IgnorePointer Implementation
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedIgnorePointerExample extends StatefulWidget {
const LocalizedIgnorePointerExample({super.key});
@override
State<LocalizedIgnorePointerExample> createState() =>
_LocalizedIgnorePointerExampleState();
}
class _LocalizedIgnorePointerExampleState
extends State<LocalizedIgnorePointerExample> {
bool _isLoading = false;
Future<void> _handleSubmit() async {
setState(() => _isLoading = true);
await Future.delayed(const Duration(seconds: 2));
setState(() => _isLoading = false);
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return IgnorePointer(
ignoring: _isLoading,
child: Opacity(
opacity: _isLoading ? 0.5 : 1.0,
child: Column(
children: [
TextField(
decoration: InputDecoration(
labelText: l10n.emailLabel,
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 16),
FilledButton(
onPressed: _handleSubmit,
child: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Text(l10n.submitButton),
),
],
),
),
);
}
}
Disabled Form Patterns
Disable Entire Form During Submission
class LocalizedDisabledForm extends StatefulWidget {
const LocalizedDisabledForm({super.key});
@override
State<LocalizedDisabledForm> createState() => _LocalizedDisabledFormState();
}
class _LocalizedDisabledFormState extends State<LocalizedDisabledForm> {
bool _isSubmitting = false;
final _formKey = GlobalKey<FormState>();
Future<void> _submit() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _isSubmitting = true);
await Future.delayed(const Duration(seconds: 2));
setState(() => _isSubmitting = false);
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Stack(
children: [
IgnorePointer(
ignoring: _isSubmitting,
child: AnimatedOpacity(
opacity: _isSubmitting ? 0.5 : 1.0,
duration: const Duration(milliseconds: 200),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
decoration: InputDecoration(
labelText: l10n.nameLabel,
border: const OutlineInputBorder(),
),
validator: (value) {
if (value?.isEmpty ?? true) {
return l10n.nameRequired;
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
decoration: InputDecoration(
labelText: l10n.emailLabel,
border: const OutlineInputBorder(),
),
validator: (value) {
if (value?.isEmpty ?? true) {
return l10n.emailRequired;
}
return null;
},
),
const SizedBox(height: 24),
FilledButton(
onPressed: _submit,
child: Text(l10n.submitButton),
),
],
),
),
),
),
if (_isSubmitting)
Positioned.fill(
child: Center(
child: Card(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text(l10n.submittingMessage),
],
),
),
),
),
),
],
);
}
}
Conditional Field Disabling
class ConditionalFieldForm extends StatefulWidget {
const ConditionalFieldForm({super.key});
@override
State<ConditionalFieldForm> createState() => _ConditionalFieldFormState();
}
class _ConditionalFieldFormState extends State<ConditionalFieldForm> {
bool _useCustomAddress = false;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SwitchListTile(
title: Text(l10n.useCustomAddressLabel),
value: _useCustomAddress,
onChanged: (value) {
setState(() => _useCustomAddress = value);
},
),
const SizedBox(height: 16),
IgnorePointer(
ignoring: !_useCustomAddress,
child: AnimatedOpacity(
opacity: _useCustomAddress ? 1.0 : 0.4,
duration: const Duration(milliseconds: 200),
child: Column(
children: [
TextField(
decoration: InputDecoration(
labelText: l10n.streetLabel,
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
decoration: InputDecoration(
labelText: l10n.cityLabel,
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
decoration: InputDecoration(
labelText: l10n.postalCodeLabel,
border: const OutlineInputBorder(),
),
),
],
),
),
),
],
);
}
}
Overlay Patterns
Non-Blocking Information Overlay
class LocalizedInfoOverlay extends StatelessWidget {
final Widget child;
final bool showOverlay;
final String message;
const LocalizedInfoOverlay({
super.key,
required this.child,
required this.showOverlay,
required this.message,
});
@override
Widget build(BuildContext context) {
return Stack(
children: [
child,
if (showOverlay)
Positioned(
bottom: 16,
left: 16,
right: 16,
child: IgnorePointer(
child: Card(
color: Theme.of(context).colorScheme.inverseSurface,
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
message,
style: TextStyle(
color: Theme.of(context).colorScheme.onInverseSurface,
),
textAlign: TextAlign.center,
),
),
),
),
),
],
);
}
}
class LocalizedMapWithInfo extends StatelessWidget {
const LocalizedMapWithInfo({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return LocalizedInfoOverlay(
showOverlay: true,
message: l10n.mapInteractionHint,
child: Container(
height: 300,
color: Colors.grey[300],
child: Center(
child: Text(l10n.mapPlaceholder),
),
),
);
}
}
Watermark Overlay
class LocalizedWatermarkOverlay extends StatelessWidget {
final Widget child;
final String watermarkText;
const LocalizedWatermarkOverlay({
super.key,
required this.child,
required this.watermarkText,
});
@override
Widget build(BuildContext context) {
return Stack(
children: [
child,
Positioned.fill(
child: IgnorePointer(
child: Center(
child: Transform.rotate(
angle: -0.3,
child: Opacity(
opacity: 0.1,
child: Text(
watermarkText,
style: Theme.of(context).textTheme.displayLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
),
),
),
),
),
],
);
}
}
class DocumentPreview extends StatelessWidget {
const DocumentPreview({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return LocalizedWatermarkOverlay(
watermarkText: l10n.draftWatermark,
child: Card(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.documentTitle,
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 16),
Text(l10n.documentContent),
],
),
),
),
);
}
}
Tutorial and Onboarding
Spotlight Tutorial Pattern
class LocalizedSpotlightTutorial extends StatefulWidget {
const LocalizedSpotlightTutorial({super.key});
@override
State<LocalizedSpotlightTutorial> createState() =>
_LocalizedSpotlightTutorialState();
}
class _LocalizedSpotlightTutorialState
extends State<LocalizedSpotlightTutorial> {
int _tutorialStep = 0;
bool _showTutorial = true;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Stack(
children: [
IgnorePointer(
ignoring: _showTutorial,
child: Scaffold(
appBar: AppBar(
title: Text(l10n.appTitle),
actions: [
IconButton(
icon: const Icon(Icons.search),
onPressed: () {},
),
IconButton(
icon: const Icon(Icons.more_vert),
onPressed: () {},
),
],
),
body: ListView(
children: [
ListTile(
title: Text(l10n.menuItem1),
leading: const Icon(Icons.home),
),
ListTile(
title: Text(l10n.menuItem2),
leading: const Icon(Icons.favorite),
),
ListTile(
title: Text(l10n.menuItem3),
leading: const Icon(Icons.settings),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
),
),
),
if (_showTutorial)
GestureDetector(
onTap: () {
if (_tutorialStep < 2) {
setState(() => _tutorialStep++);
} else {
setState(() => _showTutorial = false);
}
},
child: Container(
color: Colors.black54,
child: Center(
child: Card(
margin: const EdgeInsets.all(32),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
_getTutorialTitle(l10n),
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 12),
Text(
_getTutorialMessage(l10n),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
Text(
l10n.tapToContinue,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
),
),
),
),
],
);
}
String _getTutorialTitle(AppLocalizations l10n) {
switch (_tutorialStep) {
case 0:
return l10n.tutorialStep1Title;
case 1:
return l10n.tutorialStep2Title;
case 2:
return l10n.tutorialStep3Title;
default:
return '';
}
}
String _getTutorialMessage(AppLocalizations l10n) {
switch (_tutorialStep) {
case 0:
return l10n.tutorialStep1Message;
case 1:
return l10n.tutorialStep2Message;
case 2:
return l10n.tutorialStep3Message;
default:
return '';
}
}
}
Loading States
Full Screen Loading Blocker
class LocalizedLoadingBlocker extends StatelessWidget {
final Widget child;
final bool isLoading;
final String? loadingMessage;
const LocalizedLoadingBlocker({
super.key,
required this.child,
required this.isLoading,
this.loadingMessage,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Stack(
children: [
IgnorePointer(
ignoring: isLoading,
child: AnimatedOpacity(
opacity: isLoading ? 0.3 : 1.0,
duration: const Duration(milliseconds: 200),
child: child,
),
),
if (isLoading)
Positioned.fill(
child: Container(
color: Colors.transparent,
child: Center(
child: Card(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text(loadingMessage ?? l10n.defaultLoadingMessage),
],
),
),
),
),
),
),
],
);
}
}
class LocalizedDataScreen extends StatefulWidget {
const LocalizedDataScreen({super.key});
@override
State<LocalizedDataScreen> createState() => _LocalizedDataScreenState();
}
class _LocalizedDataScreenState extends State<LocalizedDataScreen> {
bool _isLoading = false;
Future<void> _loadData() async {
setState(() => _isLoading = true);
await Future.delayed(const Duration(seconds: 3));
setState(() => _isLoading = false);
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return LocalizedLoadingBlocker(
isLoading: _isLoading,
loadingMessage: l10n.loadingDataMessage,
child: Scaffold(
appBar: AppBar(title: Text(l10n.dataScreenTitle)),
body: Center(
child: FilledButton(
onPressed: _loadData,
child: Text(l10n.loadDataButton),
),
),
),
);
}
}
Interactive List Items
Disable Specific List Items
class LocalizedSelectableList extends StatelessWidget {
final List<SelectableItem> items;
final ValueChanged<String> onItemSelected;
const LocalizedSelectableList({
super.key,
required this.items,
required this.onItemSelected,
});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return IgnorePointer(
ignoring: !item.isEnabled,
child: Opacity(
opacity: item.isEnabled ? 1.0 : 0.5,
child: ListTile(
title: Text(item.title),
subtitle: item.subtitle != null ? Text(item.subtitle!) : null,
leading: item.icon != null ? Icon(item.icon) : null,
trailing: item.isEnabled
? const Icon(Icons.chevron_right)
: const Icon(Icons.lock),
onTap: () => onItemSelected(item.id),
),
),
);
},
);
}
}
class SelectableItem {
final String id;
final String title;
final String? subtitle;
final IconData? icon;
final bool isEnabled;
SelectableItem({
required this.id,
required this.title,
this.subtitle,
this.icon,
this.isEnabled = true,
});
}
class LocalizedSubscriptionOptions extends StatelessWidget {
const LocalizedSubscriptionOptions({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return LocalizedSelectableList(
items: [
SelectableItem(
id: 'free',
title: l10n.freePlanTitle,
subtitle: l10n.freePlanDesc,
icon: Icons.star_border,
isEnabled: true,
),
SelectableItem(
id: 'basic',
title: l10n.basicPlanTitle,
subtitle: l10n.basicPlanDesc,
icon: Icons.star_half,
isEnabled: true,
),
SelectableItem(
id: 'premium',
title: l10n.premiumPlanTitle,
subtitle: l10n.premiumPlanLocked,
icon: Icons.star,
isEnabled: false,
),
],
onItemSelected: (id) {
// Handle selection
},
);
}
}
ARB File Structure
English (app_en.arb)
{
"@@locale": "en",
"emailLabel": "Email",
"submitButton": "Submit",
"nameLabel": "Name",
"nameRequired": "Name is required",
"emailRequired": "Email is required",
"submittingMessage": "Submitting...",
"useCustomAddressLabel": "Use custom address",
"streetLabel": "Street",
"cityLabel": "City",
"postalCodeLabel": "Postal Code",
"mapInteractionHint": "Pinch to zoom, drag to pan",
"mapPlaceholder": "Map View",
"draftWatermark": "DRAFT",
"documentTitle": "Document Preview",
"documentContent": "This is a preview of your document content.",
"appTitle": "My App",
"menuItem1": "Home",
"menuItem2": "Favorites",
"menuItem3": "Settings",
"tapToContinue": "Tap anywhere to continue",
"tutorialStep1Title": "Welcome!",
"tutorialStep1Message": "Let us show you around the app.",
"tutorialStep2Title": "Navigation",
"tutorialStep2Message": "Use the menu to access different sections.",
"tutorialStep3Title": "Get Started",
"tutorialStep3Message": "You're all set! Enjoy using the app.",
"defaultLoadingMessage": "Loading...",
"loadingDataMessage": "Loading your data...",
"dataScreenTitle": "Data Screen",
"loadDataButton": "Load Data",
"freePlanTitle": "Free Plan",
"freePlanDesc": "Basic features included",
"basicPlanTitle": "Basic Plan",
"basicPlanDesc": "$9.99/month",
"premiumPlanTitle": "Premium Plan",
"premiumPlanLocked": "Coming soon"
}
German (app_de.arb)
{
"@@locale": "de",
"emailLabel": "E-Mail",
"submitButton": "Absenden",
"nameLabel": "Name",
"nameRequired": "Name ist erforderlich",
"emailRequired": "E-Mail ist erforderlich",
"submittingMessage": "Wird gesendet...",
"useCustomAddressLabel": "Eigene Adresse verwenden",
"streetLabel": "Straße",
"cityLabel": "Stadt",
"postalCodeLabel": "Postleitzahl",
"mapInteractionHint": "Zum Zoomen zusammendrücken, zum Schwenken ziehen",
"mapPlaceholder": "Kartenansicht",
"draftWatermark": "ENTWURF",
"documentTitle": "Dokumentvorschau",
"documentContent": "Dies ist eine Vorschau Ihres Dokumentinhalts.",
"appTitle": "Meine App",
"menuItem1": "Startseite",
"menuItem2": "Favoriten",
"menuItem3": "Einstellungen",
"tapToContinue": "Tippen Sie zum Fortfahren",
"tutorialStep1Title": "Willkommen!",
"tutorialStep1Message": "Wir zeigen Ihnen die App.",
"tutorialStep2Title": "Navigation",
"tutorialStep2Message": "Verwenden Sie das Menü für verschiedene Bereiche.",
"tutorialStep3Title": "Los geht's",
"tutorialStep3Message": "Alles bereit! Viel Spaß mit der App.",
"defaultLoadingMessage": "Laden...",
"loadingDataMessage": "Ihre Daten werden geladen...",
"dataScreenTitle": "Datenbildschirm",
"loadDataButton": "Daten laden",
"freePlanTitle": "Kostenloser Plan",
"freePlanDesc": "Grundfunktionen enthalten",
"basicPlanTitle": "Basis-Plan",
"basicPlanDesc": "9,99€/Monat",
"premiumPlanTitle": "Premium-Plan",
"premiumPlanLocked": "Demnächst verfügbar"
}
Arabic (app_ar.arb)
{
"@@locale": "ar",
"emailLabel": "البريد الإلكتروني",
"submitButton": "إرسال",
"nameLabel": "الاسم",
"nameRequired": "الاسم مطلوب",
"emailRequired": "البريد الإلكتروني مطلوب",
"submittingMessage": "جاري الإرسال...",
"useCustomAddressLabel": "استخدام عنوان مخصص",
"streetLabel": "الشارع",
"cityLabel": "المدينة",
"postalCodeLabel": "الرمز البريدي",
"mapInteractionHint": "اضغط للتكبير، اسحب للتحريك",
"mapPlaceholder": "عرض الخريطة",
"draftWatermark": "مسودة",
"documentTitle": "معاينة المستند",
"documentContent": "هذه معاينة لمحتوى المستند الخاص بك.",
"appTitle": "تطبيقي",
"menuItem1": "الرئيسية",
"menuItem2": "المفضلة",
"menuItem3": "الإعدادات",
"tapToContinue": "انقر في أي مكان للمتابعة",
"tutorialStep1Title": "مرحباً!",
"tutorialStep1Message": "دعنا نأخذك في جولة في التطبيق.",
"tutorialStep2Title": "التنقل",
"tutorialStep2Message": "استخدم القائمة للوصول إلى الأقسام المختلفة.",
"tutorialStep3Title": "ابدأ الآن",
"tutorialStep3Message": "كل شيء جاهز! استمتع باستخدام التطبيق.",
"defaultLoadingMessage": "جاري التحميل...",
"loadingDataMessage": "جاري تحميل بياناتك...",
"dataScreenTitle": "شاشة البيانات",
"loadDataButton": "تحميل البيانات",
"freePlanTitle": "الخطة المجانية",
"freePlanDesc": "الميزات الأساسية متضمنة",
"basicPlanTitle": "الخطة الأساسية",
"basicPlanDesc": "9.99 دولار/شهر",
"premiumPlanTitle": "الخطة المميزة",
"premiumPlanLocked": "قريباً"
}
Best Practices Summary
Do's
- Combine with Opacity for clear visual disabled states
- Use for form submission states to prevent double-submits
- Apply to overlays that shouldn't block interactions
- Create tutorial patterns that work across languages
- Test touch interactions on actual devices
Don'ts
- Don't use alone without visual feedback - users need to see disabled state
- Don't forget accessibility - consider screen readers
- Don't block critical interactions without clear feedback
- Don't use for security - it's a UI pattern, not access control
Conclusion
IgnorePointer is an essential widget for managing touch interactions in multilingual Flutter applications. By combining it with visual feedback like opacity changes, you create universally understood disabled states that communicate effectively without relying on translated text. Use IgnorePointer for loading states, tutorials, and conditional form controls to build intuitive experiences across all languages.