Ever seen those beautiful, smooth, and organic-looking waves in app UIs and wondered how they’re made? Often, the answer is Bezier curves. They are a fantastic way to move beyond simple straight lines and boxes to create fluid, dynamic shapes that make your app’s design stand out.
In Flutter, the key to creating these custom shapes is the ClipPath
widget combined with a custom clipper. This guide will walk you through exactly how to create your own beautiful wave effects from scratch.
The Core Concept: ClipPath
and CustomClipper
To clip a widget into a custom shape, Flutter gives us the ClipPath
widget. It takes two main properties:
child
: The widget you want to clip (e.g., aContainer
with a color or gradient).clipper
: An object that defines the shape of the clip. This is where the magic happens.
The clipper
must be an instance of a class that extends CustomClipper<Path>
. This custom class is where we will define the actual path of our wave.
Step 1: Create a Custom WaveClipper
Class
First, let’s create a new class, which we’ll call WaveClipper
, that will contain the logic for our wave shape.
A CustomClipper
class requires you to implement two methods:
getClip(Size size)
: This method is where you define the shape. It returns aPath
. Thesize
parameter gives you the height and width of the child widget you are clipping, allowing you to make your shape responsive.shouldReclip(CustomClipper<Path> oldClipper)
: This method is called whenever theClipPath
‘s properties change. It’s an optimization. For a static wave like ours, we can simply returnfalse
because the path doesn’t need to be recalculated unless the clipper itself changes.
Here is the basic structure:
import 'package:flutter/material.dart';
class WaveClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
// This is where we will build our wave path
}
@override
bool shouldReclip(CustomClipper<Path> oldClipper) {
return false; // We don't need to reclip for this static shape
}
}
DartStep 2: Drawing the Bezier Curve Wave
Now for the fun part! Inside the getClip
method, we’ll use a Path
object to draw our shape. A quadratic Bezier curve is perfect for a simple wave. It’s defined by a start point, an end point, and a single “control point” that pulls the line towards it to create the curve.
Here’s the logic for drawing a single wave:
@override
Path getClip(Size size) {
final path = Path();
// 1. Start from the top-left corner
path.lineTo(0, size.height * 0.8); // Go down 80% of the height
// 2. Draw the Bezier curve
// The control point is in the middle of the width and a bit lower than the start
var firstControlPoint = Offset(size.width / 4, size.height);
var firstEndPoint = Offset(size.width / 2, size.height * 0.85);
path.quadraticBezierTo(
firstControlPoint.dx,
firstControlPoint.dy,
firstEndPoint.dx,
firstEndPoint.dy,
);
// 3. Draw a second Bezier curve for a more complex wave
var secondControlPoint = Offset(size.width * 0.75, size.height * 0.7);
var secondEndPoint = Offset(size.width, size.height * 0.8);
path.quadraticBezierTo(
secondControlPoint.dx,
secondControlPoint.dy,
secondEndPoint.dx,
secondEndPoint.dy,
);
// 4. Go to the top-right corner
path.lineTo(size.width, 0);
// 5. Close the path
path.close();
return path;
}
DartThis code creates a path that forms a shape with a double-humped wave along the bottom.
Step 3: Putting It All Together
Now, let’s use our WaveClipper
with a ClipPath
widget to clip a Container
. A Stack
is perfect for placing the wave at the top of the screen behind other content.
Full Code Example
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: WaveScreen(),
);
}
}
class WaveScreen extends StatelessWidget {
const WaveScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
// The main content of the screen
const Center(
child: Text(
'Your Content Here',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
),
// The wave at the top
ClipPath(
clipper: WaveClipper(),
child: Container(
height: 220, // The height of the wave
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue, Colors.lightBlueAccent],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
),
),
],
),
);
}
}
// --- Place the WaveClipper class from Step 2 here ---
class WaveClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
final path = Path();
path.lineTo(0, size.height * 0.8);
var firstControlPoint = Offset(size.width / 4, size.height);
var firstEndPoint = Offset(size.width / 2, size.height * 0.85);
path.quadraticBezierTo(
firstControlPoint.dx,
firstControlPoint.dy,
firstEndPoint.dx,
firstEndPoint.dy,
);
var secondControlPoint = Offset(size.width * 0.75, size.height * 0.7);
var secondEndPoint = Offset(size.width, size.height * 0.8);
path.quadraticBezierTo(
secondControlPoint.dx,
secondControlPoint.dy,
secondEndPoint.dx,
secondEndPoint.dy,
);
path.lineTo(size.width, 0);
path.close();
return path;
}
@override
bool shouldReclip(CustomClipper<Path> oldClipper) {
return false;
}
}
DartConclusion
And there you have it! By defining a custom path with Bezier curves, you can clip any widget into a beautiful, fluid shape. This technique opens up a world of creative possibilities for your app’s UI.
Don’t be afraid to experiment! Adjust the x
and y
coordinates of the control points and end points in your WaveClipper
to create all kinds of unique and stunning wave effects. Happy fluttering!