Custom Widgets & Custom Painting

Gradients & Shaders on Canvas

16 min Lesson 6 of 12

Gradients & Shaders on Canvas

Flutter's CustomPainter API gives you low-level access to the Canvas object, where every pixel can be painted with full control. One of the most visually impactful techniques available is applying gradient shaders to a Paint object. Instead of filling shapes with a flat colour, you attach a Shader produced by a gradient, and the GPU smoothly interpolates colours across the painted region.

Dart's dart:ui library exposes three gradient classes: Gradient.linear, Gradient.radial, and Gradient.sweep. Each returns a Shader that you assign to Paint.shader. When you draw a rectangle, circle, path, or any other shape with that paint, the shader determines the colour at every point.

Note: A Shader is defined in local canvas coordinates. If you translate, scale, or rotate the canvas before drawing, the shader coordinates move with the canvas transformation. Keep this in mind when aligning gradients to specific screen regions.

LinearGradient Shader

A linear gradient blends colours along a straight line defined by two points. You specify the start point, the end point, a list of colours, and optional stop positions. The tileMode parameter controls what happens beyond the gradient's endpoints.

LinearGradient applied to a rectangle

class LinearGradientPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final rect = Offset.zero & size;

    final shader = const LinearGradient(
      begin: Alignment.topLeft,
      end: Alignment.bottomRight,
      colors: [Color(0xFF6A11CB), Color(0xFF2575FC)],
    ).createShader(rect);

    final paint = Paint()..shader = shader;
    canvas.drawRect(rect, paint);

    // Draw a rounded rectangle with the same gradient
    final rrect = RRect.fromRectAndRadius(
      rect.deflate(24),
      const Radius.circular(16),
    );
    canvas.drawRRect(rrect, paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
Tip: Offset.zero & size is a shorthand for Rect.fromLTWH(0, 0, size.width, size.height). Passing this rect to createShader anchors the gradient exactly within the painted area.

RadialGradient Shader

A radial gradient radiates outward from a focal centre point. You control the centre, the radius, optional focal point, the list of colours, and the stop positions. A focal point different from the centre creates a cone-like distortion useful for lens-flare effects.

RadialGradient for a glowing circle

class RadialGradientPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final center = Offset(size.width / 2, size.height / 2);
    final radius = size.shortestSide / 2;
    final rect = Rect.fromCircle(center: center, radius: radius);

    final shader = const RadialGradient(
      center: Alignment.center,
      radius: 1.0,
      colors: [
        Color(0xFFFFE259),
        Color(0xFFFFA751),
        Color(0xFF00000000),
      ],
      stops: [0.0, 0.6, 1.0],
    ).createShader(rect);

    final paint = Paint()..shader = shader;
    canvas.drawCircle(center, radius, paint);
  }

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

SweepGradient Shader

A sweep gradient rotates colours around a central point, like the hands of a clock sweeping from startAngle to endAngle (in radians). It is ideal for arc gauges, colour wheels, and ring indicators.

SweepGradient on an arc / colour wheel

import 'dart:math' as math;

class SweepGradientPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final center = Offset(size.width / 2, size.height / 2);
    final radius = size.shortestSide / 2 - 8;
    final rect = Rect.fromCircle(center: center, radius: radius);

    final shader = SweepGradient(
      center: Alignment.center,
      startAngle: 0.0,
      endAngle: math.pi * 2,
      colors: const [
        Color(0xFFFF0000),
        Color(0xFFFFFF00),
        Color(0xFF00FF00),
        Color(0xFF00FFFF),
        Color(0xFF0000FF),
        Color(0xFFFF00FF),
        Color(0xFFFF0000),
      ],
    ).createShader(rect);

    final paint = Paint()
      ..shader = shader
      ..style = PaintingStyle.stroke
      ..strokeWidth = 16;

    canvas.drawCircle(center, radius, paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
Warning: createShader(rect) uses the provided Rect to map gradient coordinates into canvas space. If the rect you pass does not match the shape you draw, the gradient will appear misaligned. Always derive the shader rect from the same bounds you intend to paint.

TileMode: Controlling Gradient Repetition

All three gradient types accept a tileMode parameter that controls how the gradient behaves beyond its defined bounds:

  • TileMode.clamp (default) — extends the edge colour outward indefinitely.
  • TileMode.repeated — tiles the gradient seamlessly, creating a striped effect.
  • TileMode.mirror — alternately mirrors the gradient for smooth tiling.
  • TileMode.decal — renders transparent outside the gradient region (Flutter 3+).

Combining Multiple Shaders with Layers

The canvas has no built-in "multiply shaders" API, but you can layer effects by saving and restoring canvas layers with canvas.saveLayer and blend modes. A common pattern is to draw a gradient base, then overlay a second paint with BlendMode.multiply or BlendMode.overlay to achieve rich, multi-colour depth.

Summary

Gradient shaders elevate custom painting from flat fills to GPU-accelerated colour transitions. The key workflow is: create the gradient object → call createShader(rect) → assign the result to Paint.shader → draw shapes with that paint. Use LinearGradient for directional sweeps, RadialGradient for circular glows and spotlights, and SweepGradient for rotational arcs and colour wheels. Always align the shader rect to the drawn region, and leverage TileMode to control edge behaviour.