ClipPath in Flutter: Create Custom Shapes Tutorial (2026)
Last Updated: | Author: Deep Singh
Flutter gives you a lot of freedom when it comes to UI, but sometimes you need a shape that isn't a rectangle, circle, or rounded box. That's where ClipPath comes in. It lets you cut any widget into an arbitrary shape defined by a custom path.
In this tutorial, you'll learn how to use ClipPath and CustomClipper<Path> to build a profile card with a curved, wave-like background. By the end, you'll understand how to define your own paths and reuse them across your app.
How ClipPath Works
A ClipPath widget takes two things:
- A clipper — an object that describes the shape you want to cut.
- A child — any widget you want to display inside that shape.
The clipper extends CustomClipper<Path> and implements two methods: getClip(), which returns the Path that defines the shape, and shouldReclip(), which tells Flutter whether to re-cut the child when the widget rebuilds.
Everything outside the path gets cut away, so the child is displayed only within the shape you define.
The Full Code
Here's the complete, working example. It creates a profile card with a gradient background that has a curved top, a circular avatar, and centered text.
import 'package:flutter/material.dart';
void main() {
runApp(const MaterialApp(
debugShowCheckedModeBanner: false,
home: MyCustomShape(),
));
}
class MyCustomShape extends StatelessWidget {
const MyCustomShape({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Custom Shape Tutorial'),
),
body: Stack(
children: [
// The custom-shaped gradient background
Center(
child: ClipPath(
clipper: MyCustomPath(),
child: Container(
width: 350,
height: 500,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.deepOrangeAccent,
Colors.orange,
],
),
),
),
),
),
// The circular avatar
const Positioned(
left: 100,
top: 100,
right: 100,
child: CircleAvatar(
backgroundColor: Colors.white60,
radius: 80,
child: CircleAvatar(
radius: 75,
backgroundImage: NetworkImage(
'https://example.com/your-image.jpg',
),
),
),
),
// The name and email text
const Align(
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(height: 150),
Text(
'Deep Singh',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
SizedBox(height: 8),
Text(
'deepsingh44@gmail.com',
style: TextStyle(
fontSize: 16,
color: Colors.white70,
),
),
],
),
),
],
),
);
}
}
class MyCustomPath extends CustomClipper<Path> {
@override
Path getClip(Size size) {
const double round = 50.0;
final path = Path();
// Start from the left side, slightly above the middle
path.moveTo(0, size.height * 0.50);
// Draw down the left edge with a rounded bottom-left corner
path.lineTo(0, size.height - round);
path.quadraticBezierTo(0, size.height, round, size.height);
// Draw along the bottom edge with a rounded bottom-right corner
path.lineTo(size.width - round, size.height);
path.quadraticBezierTo(
size.width,
size.height,
size.width,
size.height - round,
);
// Draw up the right edge to the top
path.lineTo(size.width, 0);
// Curve back down to the starting point to create the top curve
path.quadraticBezierTo(
0,
size.height * 0.33 + round,
0,
size.height * 0.33 + round * 2,
);
return path;
}
@override
bool shouldReclip(covariant CustomClipper<Path> oldClipper) => false;
}
Understanding the Path
The path is the heart of the shape. Let's break down what each line does:
1. Starting Point
path.moveTo(0, size.height * 0.50);
We start the path at the left edge, halfway down the height. This is where the curve will eventually come back to.
2. Left and Bottom Edges
path.lineTo(0, size.height - round);
path.quadraticBezierTo(0, size.height, round, size.height);
We draw straight down the left edge, then use a quadratic Bézier curve to round the bottom-left corner. The round variable controls how big the corner radius is.
3. Bottom and Right Edges
path.lineTo(size.width - round, size.height);
path.quadraticBezierTo(size.width, size.height, size.width, size.height - round);
path.lineTo(size.width, 0);
We draw along the bottom, round the bottom-right corner, and go up the right edge all the way to the top.
4. The Top Curve
path.quadraticBezierTo(
0,
size.height * 0.33 + round,
0,
size.height * 0.33 + round * 2,
);
This is what gives the shape its distinctive top curve. The control point is at (0, size.height * 0.33 + round) and the endpoint is at (0, size.height * 0.33 + round * 2). Because both points are on the left edge, the curve bulges inward, creating a concave top.
5. Reclipping
bool shouldReclip(covariant CustomClipper<Path> oldClipper) => false;
This returns false because the shape never changes. If your shape depended on some state that could change, you'd return true to tell Flutter to re-cut the child.
Experimenting with Your Own Shapes
Once you understand the basics, you can build almost any shape. Here are some ideas to try:
- Change the corner radius: Adjust the
roundvariable to make the corners sharper or softer. - Add a wave to the top: Replace the top quadratic curve with a cubic Bézier (
cubicTo) for more control. - Cut out the middle: Use multiple paths with
Path.fillType = PathFillType.evenOddto create holes. - Animate the shape: Use
AnimatedClipPath(via aTweenor anAnimationController) to morph between two paths.
A common variation is the wave shape at the bottom of a card. Here's what that path looks like:
final path = Path();
path.lineTo(0, size.height - 40);
path.quadraticBezierTo(
size.width / 4,
size.height,
size.width / 2,
size.height,
);
path.quadraticBezierTo(
size.width - (size.width / 4),
size.height,
size.width,
size.height - 40,
);
path.lineTo(size.width, 0);
return path;
This creates a smooth wave that dips down at the bottom edge — perfect for headers or footers.
Wrapping Up
You now know how to use ClipPath and CustomClipper<Path> to create custom shapes in Flutter. This is a powerful tool for building unique UIs that stand out from the default Material look.
Remember a few things as you build on this:
- Performance: Clipping is relatively cheap, but complex paths with many curves can impact performance on low-end devices. Test on real hardware.
- Reuse: Once you've built a clipper you like, save it as a separate class so you can reuse it across your app.
- Hit testing: Clicks and taps inside the clipped area still register on the original widget bounds. If you need precise hit testing, use
ClipPathwith a customhitTestor wrap your child in aGestureDetectorwith a matching shape.