Flutter

Blob shapes in Flutter

Use the exported CustomPainter, scale the path from a 500 by 500 space to any widget size, clip images with ClipPath and fill a blob with a gradient.

Updated September 18, 2026

The Flutter export is a CustomPainter that builds the shape with moveTo and quadraticBezierTo, the same two commands the SVG path uses. Nothing outside the Flutter SDK is required.

The exported painter

Paths are written in the 500 by 500 space the blob was designed in, then multiplied by the ratio between that space and the widget size. Keeping the path in its own function lets both the painter and the clipper below use it.

import 'package:flutter/material.dart';

Path blobPath(Size size) {
  final w = size.width / 500.0;
  final h = size.height / 500.0;

  return Path()
    ..moveTo(413 * w, 84.5 * h)
    ..quadraticBezierTo(476 * w, 169 * h, 447 * w, 249.5 * h)
    ..quadraticBezierTo(418 * w, 330 * h, 349 * w, 383 * h)
    ..quadraticBezierTo(280 * w, 436 * h, 204 * w, 412 * h)
    ..quadraticBezierTo(128 * w, 388 * h, 96 * w, 314 * h)
    ..quadraticBezierTo(64 * w, 240 * h, 105 * w, 169.5 * h)
    ..quadraticBezierTo(146 * w, 99 * h, 220 * w, 74.5 * h)
    ..quadraticBezierTo(294 * w, 50 * h, 353.5 * w, 67.25 * h)
    ..quadraticBezierTo(413 * w, 84.5 * h, 413 * w, 84.5 * h)
    ..close();
}

class BlobPainter extends CustomPainter {
  const BlobPainter({this.color = const Color(0xFF1E8FE5)});

  final Color color;

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = color
      ..isAntiAlias = true;
    canvas.drawPath(blobPath(size), paint);
  }

  @override
  bool shouldRepaint(covariant BlobPainter oldDelegate) => oldDelegate.color != color;
}

Width and height scale independently, so a non-square box stretches the shape. Wrap the painter in an AspectRatio widget with a ratio of 1 when you want the proportions kept.

Save the export as its own file, for example lib/widgets/blob_painter.dart, and import it where you need it. It depends on nothing outside the Flutter SDK, so it adds no entry to pubspec.yaml and nothing to your build.

Draw it in a widget

CustomPaint needs a size. Give it one directly, or let it fill a parent that already has bounds.

SizedBox(
  width: 240,
  height: 240,
  child: CustomPaint(
    painter: const BlobPainter(color: Color(0xFF0668B6)),
  ),
)

Because the painter is const and shouldRepaint only returns true when the color changes, rebuilds elsewhere in the tree cost nothing.

As a background, put the CustomPaint at the bottom of a Stack and position it so part of the shape runs off the edge of the screen, which is what makes a blob read as a backdrop rather than a sticker. Positioned with negative offsets does the job, and IgnorePointer keeps it out of the way of taps.

Clip an image or any widget

A CustomClipper reuses the same path, so the shape stays identical between a painted blob and a clipped one.

class BlobClipper extends CustomClipper<Path> {
  const BlobClipper();

  @override
  Path getClip(Size size) => blobPath(size);

  @override
  bool shouldReclip(covariant BlobClipper oldClipper) => false;
}

ClipPath(
  clipper: const BlobClipper(),
  child: Image.network(
    'https://example.com/portrait.jpg',
    width: 240,
    height: 240,
    fit: BoxFit.cover,
  ),
)

Anything can go inside: a Container with a color, a video, a Stack of layers. Add clipBehavior: Clip.antiAlias to soften the edge on photographs.

Gradients

Solid colors set the color field on Paint. For the two-stop vertical gradient the generator exports, set a shader instead and draw it from the top edge to the bottom edge of the widget.

import 'dart:ui' as ui;

class GradientBlobPainter extends CustomPainter {
  const GradientBlobPainter();

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..shader = ui.Gradient.linear(
        Offset(size.width / 2, 0),
        Offset(size.width / 2, size.height),
        const [Color(0xFFC2E59C), Color(0xFF64B3F4)],
      );
    canvas.drawPath(blobPath(size), paint);
  }

  @override
  bool shouldRepaint(covariant GradientBlobPainter oldDelegate) => false;
}

Colors are ARGB, so a hex value from the generator gains an FF prefix for full opacity. Pass a third argument of stop positions if you want the handover somewhere other than the midpoint.

Outlines and stroke width

The outline toggle draws the shape as a stroke 7 units wide in the 500-unit space. Scale that number the same way the coordinates are scaled, or the border thins out on large widgets.

final paint = Paint()
  ..style = PaintingStyle.stroke
  ..strokeWidth = 7 * (size.width / 500.0)
  ..color = const Color(0xFF344F5C);

Half of a stroke sits outside the path, so inset the widget by half the stroke width if the edge is being clipped by its parent.

Common questions

Why is my blob flattened? The path scales width and height separately. A 300 by 120 box squashes the shape by design. Use AspectRatio, a square SizedBox, or scale both axes by the shorter side.

Can I animate between two blobs? Yes, if both were exported at the same Complexity value. Equal node counts mean equal numbers of control points, so you can interpolate each coordinate with an AnimationController and rebuild the path every frame.

Does this work on web and desktop builds? The painter uses only dart:ui and the Flutter painting layer, so it renders on every target Flutter supports, including web, macOS, Windows and Linux.