In Flutter, the ripple effect (also known as the ink splash or material ripple) is part of the Material Design guidelines and is usually seen when a user taps on a button, list item, or surface. Flutter provides multiple ways to implement this effect, depending on your use case:
🔹 1. Using InkWell
The most common way. Wrap your widget with InkWell to get a ripple on tap.
InkWell(
onTap: () {
print("Tapped!");
},
child: Container(
padding: EdgeInsets.all(20),
child: Text("Tap Me"),
),
)
👉 Notes:
InkWellmust have a Material ancestor (e.g., inside aMaterialApp,Scaffold, orMaterialwidget).- The ripple effect respects the widget’s shape.
🔹 2. Using InkResponse
More configurable than InkWell. Useful if you need control over the ripple shape (circular vs rectangular).
InkResponse(
onTap: () {
print("Tapped!");
},
radius: 40,
containedInkWell: true,
child: Icon(Icons.favorite, size: 50),
)
👉 Difference from InkWell:
InkResponsegives more control (like splash radius, custom shapes).InkWellis a simpler wrapper built on top ofInkResponse.
🔹 3. Using Material with Ink Features
If you want the ripple behind a custom widget, wrap it with Material and apply an InkWell.
Material(
color: Colors.transparent,
child: InkWell(
splashColor: Colors.blue,
onTap: () {},
child: Padding(
padding: EdgeInsets.all(20),
child: Text("Ripple over Transparent Material"),
),
),
)
🔹 4. Using GestureDetector + Custom Ripple
If you don’t want to depend on Material’s ink effects, you can build a custom ripple animation with GestureDetector and AnimatedContainer or CustomPainter.
GestureDetector(
onTapDown: (_) {
// trigger custom animation
},
child: Container(
color: Colors.grey[200],
padding: EdgeInsets.all(20),
child: Text("Custom Ripple"),
),
)
👉 In this case, you’ll need to animate an expanding circle manually (e.g., TweenAnimationBuilder + ClipOval).
🔹 5. Using Ink widget (for painting ripples inside a Material)
If you want a background ripple effect that paints correctly inside a Material widget, use Ink.
Material(
child: Ink(
decoration: BoxDecoration(
color: Colors.grey[200],
shape: BoxShape.circle,
),
child: InkWell(
onTap: () {},
child: Padding(
padding: EdgeInsets.all(20),
child: Icon(Icons.touch_app),
),
),
),
)
🔹 6. Third-Party Animation Packages
If you want fancier ripples, you can use packages like:
These allow animated ripple backgrounds beyond Material guidelines.
✅ Best Practice: Use InkWell for standard Material buttons and InkResponse when you need more control. Go custom (GestureDetector) only if you want non-Material ripple behavior.
Flutter Buttons
Flutter’s button widgets already include the ripple effect out-of-the-box because they are built on top of InkWell / InkResponse internally.
Here’s how ripple behaves across the main button types:
🔹 1. TextButton
Minimal button with ripple on tap.
TextButton(
onPressed: () {},
child: Text("Text Button"),
)
- Transparent background, only text + ripple.
- Replaces the old
FlatButton.
🔹 2. ElevatedButton
Button with elevation + ripple.
ElevatedButton(
onPressed: () {},
child: Text("Elevated Button"),
)
- Background + shadow.
- Replaces the old
RaisedButton.
🔹 3. OutlinedButton
Button with a border + ripple.
OutlinedButton(
onPressed: () {},
child: Text("Outlined Button"),
)
- Ripple shows over the outlined border.
- Replaces the old
OutlineButton.
🔹 4. IconButton
Icon-only button with ripple on tap.
IconButton(
icon: Icon(Icons.favorite),
onPressed: () {},
)
- Uses
InkResponseinternally for circular ripples.
🔹 5. FloatingActionButton (FAB)
Circular button with ripple.
FloatingActionButton(
onPressed: () {},
child: Icon(Icons.add),
)
- Circular ripple that fits FAB shape.
🔹 6. MaterialButton (base class)
Most Material buttons extend MaterialButton → so ripple is part of its behavior.
MaterialButton(
color: Colors.blue,
textColor: Colors.white,
onPressed: () {},
child: Text("Material Button"),
)
🔹 Customizing Ripple in Buttons
You can control the ripple color and highlight color using ButtonStyle:
ElevatedButton(
style: ElevatedButton.styleFrom(
splashFactory: InkRipple.splashFactory, // default ripple
foregroundColor: Colors.white,
backgroundColor: Colors.blue,
overlayColor: MaterialStateProperty.all(Colors.red.withOpacity(0.3)), // ripple color
),
onPressed: () {},
child: Text("Custom Ripple"),
)
overlayColor→ sets ripple & pressed state color.splashFactory→ can swap ripple type (InkRipple,InkSparkle,NoSplash).
✨ So basically:
- All Material buttons already come with ripple (no need to wrap with
InkWell). - You just tweak them with
ButtonStyleif you want to change ripple color/behavior.
Customzing Ripple Effect
You can absolutely parametrize a ripple color by making it lighter or darker than the base color. Flutter has a handy method for this:
Option 1: Use withOpacity
splashColor: backgroundColor.withOpacity(0.3), // 30% opacity
highlightColor: backgroundColor.withOpacity(0.2),
That way, even a dark background gets a visible translucent splash.
Option 2: Darken/lighten the base color
You can programmatically make a color darker or lighter by converting it to HSL (hue-saturation-lightness):
Color darken(Color color, [double amount = .1]) {
final hsl = HSLColor.fromColor(color);
final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0));
return hslDark.toColor();
}
Color lighten(Color color, [double amount = .1]) {
final hsl = HSLColor.fromColor(color);
final hslLight = hsl.withLightness((hsl.lightness + amount).clamp(0.0, 1.0));
return hslLight.toColor();
}
Then in your InkWell:
InkWell(
splashColor: lighten(backgroundColor, 0.2),
highlightColor: darken(backgroundColor, 0.2),
onTap: ...
)
Option 3: Use Material defaults
If you skip setting splashColor and highlightColor, Flutter will pick a default based on the ThemeData. Sometimes letting the theme handle contrast gives better results.
👉 I’d recommend Option 2 if you want full control and a parameterized way to always ensure the ripple is visible regardless of background.
Do you want me to make you a small reusable helper (e.g. rippleColorFrom(backgroundColor)) that automatically decides whether to darken or lighten depending on how bright the base is?
Good question 👍
By default, the ListTile ripple is the theme’s splash color, coming from the InkWell inside it.
You can override it using the properties:
tileColor→ background when not pressedselectedTileColor→ background when selectedhoverColor→ hover effect (desktop/web)focusColor→ when focusedsplashColor→ the ripple color (this is what you want!)
Example:
ListTile(
title: Text("Custom Ripple"),
onTap: () {
print("Tapped");
},
splashColor: Colors.purple.withOpacity(0.3), // ripple color
tileColor: Colors.grey[200], // background
selectedTileColor: Colors.blue[100], // when selected
)
If you want to set it globally for all ListTiles in your app, use ThemeData:
MaterialApp(
theme: ThemeData(
splashColor: Colors.red.withOpacity(0.2), // global ripple
hoverColor: Colors.red.withOpacity(0.1),
),
home: Scaffold(
body: ListView(
children: [
ListTile(title: Text("One"), onTap: () {}),
ListTile(title: Text("Two"), onTap: () {}),
],
),
),
);
Note: If you are not seeing Ripple Effect, it may be that the color you’ve set to the Pressed State is lighter than the Non-Pressed State. It happened to me – so – play around with toggling your clickable items (without actually triggering the taps). This way you will improve the UX and make your app act responsively to the user interfactions.
