Showing posts with label ClipPath in Flutter. Show all posts
Showing posts with label ClipPath in Flutter. Show all posts

ClipPath in Flutter

ClipPath in Flutter: Create Custom Shapes Tutorial (2026) | D.Tech Academic

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.

ClipPath custom shape with gradient background in Flutter
The final custom-shaped profile card

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;
}
Second custom shape variation created with ClipPath in Flutter
Another custom shape made with the same technique

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 round variable 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.evenOdd to create holes.
  • Animate the shape: Use AnimatedClipPath (via a Tween or an AnimationController) 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 ClipPath with a custom hitTest or wrap your child in a GestureDetector with a matching shape.