← Back to Blog

Flutter Ink Widget Localization: Splash Effects for Multilingual Interfaces

flutterinkmaterialeffectslocalizationstyling

Flutter Ink Widget Localization: Splash Effects for Multilingual Interfaces

Ink is a Flutter widget that paints a decoration on a Material, typically used for background colors with proper ink splash support. In multilingual applications, Ink provides themed backgrounds that work seamlessly with touch feedback across all languages and text directions.

Understanding Ink in Localization Context

Ink paints decorations on Material ancestors while respecting ink splash effects that appear over them. For multilingual apps, this enables:

  • Consistent background styling across all locales
  • Proper ink splash effects over decorated surfaces
  • Theme-aware backgrounds that respect locale preferences
  • Direction-aware gradient effects for RTL layouts

Why Ink Matters for Multilingual Apps

Ink provides:

  • Proper splash layering: Ink effects appear over Ink widgets correctly
  • Theme integration: Respects app-wide localized theming
  • Decoration support: Supports images, gradients, and colors
  • Material compliance: Works within Material Design system

Basic Ink Implementation

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

class LocalizedInkExample extends StatelessWidget {
  const LocalizedInkExample({super.key});

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

    return Material(
      child: Ink(
        decoration: BoxDecoration(
          color: Theme.of(context).colorScheme.primaryContainer,
          borderRadius: BorderRadius.circular(12),
        ),
        child: InkWell(
          onTap: () {},
          borderRadius: BorderRadius.circular(12),
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: Row(
              children: [
                Icon(
                  Icons.palette,
                  color: Theme.of(context).colorScheme.onPrimaryContainer,
                ),
                const SizedBox(width: 12),
                Text(
                  l10n.inkWithSplash,
                  style: TextStyle(
                    color: Theme.of(context).colorScheme.onPrimaryContainer,
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

Ink with Gradients

Direction-Aware Gradient Ink

class DirectionalGradientInk extends StatelessWidget {
  final Widget child;
  final List<Color> colors;
  final VoidCallback? onTap;

  const DirectionalGradientInk({
    super.key,
    required this.child,
    required this.colors,
    this.onTap,
  });

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

    return Material(
      child: Ink(
        decoration: BoxDecoration(
          gradient: LinearGradient(
            begin: isRtl ? Alignment.centerRight : Alignment.centerLeft,
            end: isRtl ? Alignment.centerLeft : Alignment.centerRight,
            colors: colors,
          ),
          borderRadius: BorderRadius.circular(12),
        ),
        child: InkWell(
          onTap: onTap,
          borderRadius: BorderRadius.circular(12),
          child: child,
        ),
      ),
    );
  }
}

class LocalizedGradientCards extends StatelessWidget {
  const LocalizedGradientCards({super.key});

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

    return Column(
      children: [
        DirectionalGradientInk(
          colors: [Colors.purple.shade400, Colors.blue.shade400],
          onTap: () {},
          child: Padding(
            padding: const EdgeInsets.all(20),
            child: Row(
              children: [
                const Icon(Icons.auto_awesome, color: Colors.white),
                const SizedBox(width: 12),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        l10n.premiumFeature,
                        style: const TextStyle(
                          color: Colors.white,
                          fontWeight: FontWeight.bold,
                          fontSize: 16,
                        ),
                      ),
                      Text(
                        l10n.premiumFeatureDesc,
                        style: const TextStyle(
                          color: Colors.white70,
                          fontSize: 12,
                        ),
                      ),
                    ],
                  ),
                ),
                const Icon(Icons.chevron_right, color: Colors.white),
              ],
            ),
          ),
        ),
        const SizedBox(height: 16),
        DirectionalGradientInk(
          colors: [Colors.orange.shade400, Colors.red.shade400],
          onTap: () {},
          child: Padding(
            padding: const EdgeInsets.all(20),
            child: Row(
              children: [
                const Icon(Icons.local_fire_department, color: Colors.white),
                const SizedBox(width: 12),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        l10n.hotDeal,
                        style: const TextStyle(
                          color: Colors.white,
                          fontWeight: FontWeight.bold,
                          fontSize: 16,
                        ),
                      ),
                      Text(
                        l10n.hotDealDesc,
                        style: const TextStyle(
                          color: Colors.white70,
                          fontSize: 12,
                        ),
                      ),
                    ],
                  ),
                ),
                const Icon(Icons.chevron_right, color: Colors.white),
              ],
            ),
          ),
        ),
      ],
    );
  }
}

Ink with Images

Image Background with Ink

class LocalizedImageInk extends StatelessWidget {
  final ImageProvider image;
  final String title;
  final String subtitle;
  final VoidCallback? onTap;

  const LocalizedImageInk({
    super.key,
    required this.image,
    required this.title,
    required this.subtitle,
    this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    return Material(
      borderRadius: BorderRadius.circular(16),
      clipBehavior: Clip.antiAlias,
      child: Ink.image(
        image: image,
        fit: BoxFit.cover,
        height: 180,
        child: InkWell(
          onTap: onTap,
          child: Container(
            decoration: BoxDecoration(
              gradient: LinearGradient(
                begin: Alignment.topCenter,
                end: Alignment.bottomCenter,
                colors: [
                  Colors.transparent,
                  Colors.black.withOpacity(0.8),
                ],
              ),
            ),
            padding: const EdgeInsets.all(16),
            alignment: Alignment.bottomLeft,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.end,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  title,
                  style: const TextStyle(
                    color: Colors.white,
                    fontSize: 18,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const SizedBox(height: 4),
                Text(
                  subtitle,
                  style: const TextStyle(
                    color: Colors.white70,
                    fontSize: 14,
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

class LocalizedImageGallery extends StatelessWidget {
  const LocalizedImageGallery({super.key});

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

    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        LocalizedImageInk(
          image: const NetworkImage('https://picsum.photos/400/180?1'),
          title: l10n.destination1,
          subtitle: l10n.destination1Desc,
          onTap: () {},
        ),
        const SizedBox(height: 16),
        LocalizedImageInk(
          image: const NetworkImage('https://picsum.photos/400/180?2'),
          title: l10n.destination2,
          subtitle: l10n.destination2Desc,
          onTap: () {},
        ),
        const SizedBox(height: 16),
        LocalizedImageInk(
          image: const NetworkImage('https://picsum.photos/400/180?3'),
          title: l10n.destination3,
          subtitle: l10n.destination3Desc,
          onTap: () {},
        ),
      ],
    );
  }
}

Themed Ink Containers

Status-Based Ink Colors

class LocalizedStatusInk extends StatelessWidget {
  final String message;
  final StatusType status;
  final VoidCallback? onTap;

  const LocalizedStatusInk({
    super.key,
    required this.message,
    required this.status,
    this.onTap,
  });

  Color _getBackgroundColor(BuildContext context) {
    switch (status) {
      case StatusType.success:
        return Colors.green.shade50;
      case StatusType.warning:
        return Colors.orange.shade50;
      case StatusType.error:
        return Colors.red.shade50;
      case StatusType.info:
        return Colors.blue.shade50;
    }
  }

  Color _getForegroundColor() {
    switch (status) {
      case StatusType.success:
        return Colors.green.shade700;
      case StatusType.warning:
        return Colors.orange.shade700;
      case StatusType.error:
        return Colors.red.shade700;
      case StatusType.info:
        return Colors.blue.shade700;
    }
  }

  IconData _getIcon() {
    switch (status) {
      case StatusType.success:
        return Icons.check_circle;
      case StatusType.warning:
        return Icons.warning;
      case StatusType.error:
        return Icons.error;
      case StatusType.info:
        return Icons.info;
    }
  }

  @override
  Widget build(BuildContext context) {
    return Material(
      child: Ink(
        decoration: BoxDecoration(
          color: _getBackgroundColor(context),
          borderRadius: BorderRadius.circular(8),
        ),
        child: InkWell(
          onTap: onTap,
          borderRadius: BorderRadius.circular(8),
          child: Padding(
            padding: const EdgeInsets.all(12),
            child: Row(
              children: [
                Icon(_getIcon(), color: _getForegroundColor()),
                const SizedBox(width: 12),
                Expanded(
                  child: Text(
                    message,
                    style: TextStyle(color: _getForegroundColor()),
                  ),
                ),
                if (onTap != null)
                  Icon(
                    Icons.chevron_right,
                    color: _getForegroundColor(),
                  ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

enum StatusType { success, warning, error, info }

class LocalizedStatusMessages extends StatelessWidget {
  const LocalizedStatusMessages({super.key});

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

    return Column(
      children: [
        LocalizedStatusInk(
          message: l10n.successMessage,
          status: StatusType.success,
          onTap: () {},
        ),
        const SizedBox(height: 8),
        LocalizedStatusInk(
          message: l10n.warningMessage,
          status: StatusType.warning,
          onTap: () {},
        ),
        const SizedBox(height: 8),
        LocalizedStatusInk(
          message: l10n.errorMessage,
          status: StatusType.error,
          onTap: () {},
        ),
        const SizedBox(height: 8),
        LocalizedStatusInk(
          message: l10n.infoMessage,
          status: StatusType.info,
        ),
      ],
    );
  }
}

Selection States

Ink with Selection Indicator

class LocalizedSelectableInk extends StatelessWidget {
  final String title;
  final String subtitle;
  final bool isSelected;
  final VoidCallback? onTap;

  const LocalizedSelectableInk({
    super.key,
    required this.title,
    required this.subtitle,
    required this.isSelected,
    this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    return Material(
      child: Ink(
        decoration: BoxDecoration(
          color: isSelected
              ? Theme.of(context).colorScheme.primaryContainer
              : Theme.of(context).colorScheme.surface,
          borderRadius: BorderRadius.circular(12),
          border: Border.all(
            color: isSelected
                ? Theme.of(context).colorScheme.primary
                : Theme.of(context).colorScheme.outline,
            width: isSelected ? 2 : 1,
          ),
        ),
        child: InkWell(
          onTap: onTap,
          borderRadius: BorderRadius.circular(12),
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: Row(
              children: [
                Container(
                  width: 24,
                  height: 24,
                  decoration: BoxDecoration(
                    shape: BoxShape.circle,
                    color: isSelected
                        ? Theme.of(context).colorScheme.primary
                        : Colors.transparent,
                    border: Border.all(
                      color: isSelected
                          ? Theme.of(context).colorScheme.primary
                          : Theme.of(context).colorScheme.outline,
                      width: 2,
                    ),
                  ),
                  child: isSelected
                      ? Icon(
                          Icons.check,
                          size: 16,
                          color: Theme.of(context).colorScheme.onPrimary,
                        )
                      : null,
                ),
                const SizedBox(width: 16),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        title,
                        style: Theme.of(context).textTheme.titleMedium,
                      ),
                      const SizedBox(height: 4),
                      Text(
                        subtitle,
                        style: Theme.of(context).textTheme.bodySmall,
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

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

  @override
  State<LocalizedPlanSelector> createState() => _LocalizedPlanSelectorState();
}

class _LocalizedPlanSelectorState extends State<LocalizedPlanSelector> {
  String _selectedPlan = 'basic';

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

    return Column(
      children: [
        LocalizedSelectableInk(
          title: l10n.freePlan,
          subtitle: l10n.freePlanDesc,
          isSelected: _selectedPlan == 'free',
          onTap: () => setState(() => _selectedPlan = 'free'),
        ),
        const SizedBox(height: 12),
        LocalizedSelectableInk(
          title: l10n.basicPlan,
          subtitle: l10n.basicPlanDesc,
          isSelected: _selectedPlan == 'basic',
          onTap: () => setState(() => _selectedPlan = 'basic'),
        ),
        const SizedBox(height: 12),
        LocalizedSelectableInk(
          title: l10n.proPlan,
          subtitle: l10n.proPlanDesc,
          isSelected: _selectedPlan == 'pro',
          onTap: () => setState(() => _selectedPlan = 'pro'),
        ),
      ],
    );
  }
}

ARB File Structure

English (app_en.arb)

{
  "@@locale": "en",

  "inkWithSplash": "Tap for splash effect",

  "premiumFeature": "Premium Features",
  "premiumFeatureDesc": "Unlock advanced capabilities",
  "hotDeal": "Hot Deal",
  "hotDealDesc": "Limited time offer - 50% off",

  "destination1": "Paris, France",
  "destination1Desc": "The City of Light",
  "destination2": "Tokyo, Japan",
  "destination2Desc": "Modern meets traditional",
  "destination3": "New York, USA",
  "destination3Desc": "The city that never sleeps",

  "successMessage": "Your changes have been saved successfully.",
  "warningMessage": "Please review your settings before continuing.",
  "errorMessage": "An error occurred. Please try again.",
  "infoMessage": "New features are available in this update.",

  "freePlan": "Free",
  "freePlanDesc": "Basic features for personal use",
  "basicPlan": "Basic",
  "basicPlanDesc": "$9.99/month - More features for growing needs",
  "proPlan": "Professional",
  "proPlanDesc": "$29.99/month - Full access to all features"
}

German (app_de.arb)

{
  "@@locale": "de",

  "inkWithSplash": "Tippen für Splash-Effekt",

  "premiumFeature": "Premium-Funktionen",
  "premiumFeatureDesc": "Erweiterte Funktionen freischalten",
  "hotDeal": "Heißes Angebot",
  "hotDealDesc": "Zeitlich begrenztes Angebot - 50% Rabatt",

  "destination1": "Paris, Frankreich",
  "destination1Desc": "Die Stadt des Lichts",
  "destination2": "Tokio, Japan",
  "destination2Desc": "Modern trifft Tradition",
  "destination3": "New York, USA",
  "destination3Desc": "Die Stadt, die niemals schläft",

  "successMessage": "Ihre Änderungen wurden erfolgreich gespeichert.",
  "warningMessage": "Bitte überprüfen Sie Ihre Einstellungen.",
  "errorMessage": "Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.",
  "infoMessage": "Neue Funktionen sind in diesem Update verfügbar.",

  "freePlan": "Kostenlos",
  "freePlanDesc": "Grundfunktionen für den persönlichen Gebrauch",
  "basicPlan": "Basis",
  "basicPlanDesc": "9,99€/Monat - Mehr Funktionen für wachsende Bedürfnisse",
  "proPlan": "Professionell",
  "proPlanDesc": "29,99€/Monat - Voller Zugang zu allen Funktionen"
}

Arabic (app_ar.arb)

{
  "@@locale": "ar",

  "inkWithSplash": "انقر لتأثير الرش",

  "premiumFeature": "ميزات مميزة",
  "premiumFeatureDesc": "فتح القدرات المتقدمة",
  "hotDeal": "عرض ساخن",
  "hotDealDesc": "عرض لفترة محدودة - خصم 50%",

  "destination1": "باريس، فرنسا",
  "destination1Desc": "مدينة النور",
  "destination2": "طوكيو، اليابان",
  "destination2Desc": "الحداثة تلتقي بالتقاليد",
  "destination3": "نيويورك، الولايات المتحدة",
  "destination3Desc": "المدينة التي لا تنام",

  "successMessage": "تم حفظ تغييراتك بنجاح.",
  "warningMessage": "يرجى مراجعة إعداداتك قبل المتابعة.",
  "errorMessage": "حدث خطأ. يرجى المحاولة مرة أخرى.",
  "infoMessage": "ميزات جديدة متاحة في هذا التحديث.",

  "freePlan": "مجاني",
  "freePlanDesc": "ميزات أساسية للاستخدام الشخصي",
  "basicPlan": "أساسي",
  "basicPlanDesc": "9.99 دولار/شهر - المزيد من الميزات للاحتياجات المتنامية",
  "proPlan": "احترافي",
  "proPlanDesc": "29.99 دولار/شهر - وصول كامل لجميع الميزات"
}

Best Practices Summary

Do's

  1. Use Ink instead of Container when you need ink splash effects
  2. Match borderRadius with InkWell for proper splash clipping
  3. Use directional gradients for RTL-aware backgrounds
  4. Combine with Material ancestor for proper ink rendering
  5. Test splash effects on actual devices

Don'ts

  1. Don't use Ink without Material ancestor
  2. Don't forget to clip with Ink.image for image backgrounds
  3. Don't mix Ink with regular Container for the same element
  4. Don't over-decorate - keep designs clean

Conclusion

Ink is essential for creating decorated surfaces with proper Material Design splash effects in multilingual Flutter applications. By using Ink for backgrounds with gradients, images, and colors, you ensure touch feedback appears correctly over your decorations. Use Ink with directional awareness for RTL layouts and consistent theming across all locales.

Further Reading