Skip to content

Instantly share code, notes, and snippets.

@rydmike
Created November 25, 2025 22:04
Show Gist options
  • Select an option

  • Save rydmike/abc6c93ec29282d766f8cc524404d6f8 to your computer and use it in GitHub Desktop.

Select an option

Save rydmike/abc6c93ec29282d766f8cc524404d6f8 to your computer and use it in GitHub Desktop.
Feature Request: Decoration-Aware ShapeBorder for Material Components

Feature Request: Decoration-Aware ShapeBorder for Material Components

Summary

Enable Material components (Card, Dialog, TextField, Chip, etc.) to render full decoration styling (gradients, multiple shadows, inner shadows, images) through their existing shape property by introducing a new ShapeBorder subclass that carries decoration data and modifying the Material widget to render it.


Use Case

The Problem

Flutter's BoxDecoration offers rich styling capabilities that developers frequently need:

  • Gradient fills (linear, radial, sweep)
  • Multiple box shadows with spread, blur, and offset
  • Inner shadows (inset shadows)
  • Background images
  • Custom shadow colors (beyond elevation-based shadows)

However, Material components cannot use BoxDecoration. Components like Card, Dialog, ElevatedButton, TextField, Chip, and others use ShapeBorder for their shape, which only defines:

  • The outline path (getOuterPath, getInnerPath)
  • Border painting (paint method)
  • Animation interpolation (lerpFrom, lerpTo)

The Material widget internally controls background painting using its own color and elevation properties, completely ignoring any custom properties a ShapeBorder subclass might carry.

Current Workarounds Are Inadequate

Developers currently resort to:

  1. Wrapper stacks - Wrapping components in Stack with DecoratedBox:

    Stack(
      children: [
        DecoratedBox(decoration: myGradientDecoration),
        Card(color: Colors.transparent, child: content),
      ],
    )
    • Breaks widget tree semantics
    • Complicates layout
    • Doesn't integrate with theming
  2. Custom widget implementations - Recreating Material components:

    • Loses Material behaviors (ink splash, focus handling, accessibility)
    • Significant development overhead
    • Diverges from framework updates
  3. ButtonStyle.backgroundBuilder (Flutter 3.22+):

    • Only works for button components
    • Requires builder function instead of declarative style
    • Cannot be used in ThemeData component themes

Real-World Impact

This limitation affects common design requirements:

  • Gradient buttons - Extremely common in modern app design
  • Neumorphic UI - Requires inner shadows
  • Glass/frosted effects - Need semi-transparent gradients
  • Custom shadow effects - Brand-specific shadow colors, multiple shadows
  • Card highlighting - Gradient borders, glow effects
  • Form field styling - Gradient backgrounds for inputs

Related issues demonstrate significant community demand:

  • #89563 - ElevatedButton filled with gradient
  • #139456 - Allow arbitrary foreground and background elements for ButtonStyle
  • #130335 - Add Decoration to ButtonStyleButton's styling

Proposal

High-Level Approach

Introduce a decoration-aware ShapeBorder system that:

  1. Creates a new OutlinedBorder subclass (DecorationShapeBorder) that carries decoration data (gradient, shadows, image, color)
  2. Modifies Material widget to detect and render decoration data from its shape property
  3. Maintains full backward compatibility - existing ShapeBorder implementations continue working unchanged
  4. Enables theming - decoration can be specified in ThemeData component themes

Additional Shape Features

As part of this enhancement, also add support for:

  • Mixed corner types - Different corner style per corner (rounded, beveled, continuous/superellipse)
  • Per-corner radius - Different radius for each corner
  • Smooth shape animation/lerping between different corner configurations

Detailed Proposal

1. New Classes in painting Library

1.1 DecorationCornerStyle Enum

/// The style of a corner in a [DecorationShapeBorder].
enum DecorationCornerStyle {
  /// Standard circular arc corner (like [RoundedRectangleBorder]).
  rounded,

  /// Cut/chamfered corner (like [BeveledRectangleBorder]).
  beveled,

  /// Smooth continuous curve (like [ContinuousRectangleBorder]).
  continuous,

  /// iOS-style squircle/superellipse (like [RoundedSuperellipseBorder]).
  superellipse,
}

1.2 DecorationCorner Class

/// Defines the style and radius of a single corner.
@immutable
class DecorationCorner {
  const DecorationCorner({
    this.style = DecorationCornerStyle.rounded,
    this.radius = Radius.zero,
  });

  /// The visual style of this corner.
  final DecorationCornerStyle style;

  /// The radius of this corner.
  final Radius radius;

  /// Linearly interpolate between two corners.
  static DecorationCorner? lerp(DecorationCorner? a, DecorationCorner? b, double t);

  // ... equality, hashCode, etc.
}

1.3 DecorationCorners Class

/// Defines the corners of a [DecorationShapeBorder].
@immutable
class DecorationCorners {
  const DecorationCorners({
    this.topLeft = const DecorationCorner(),
    this.topRight = const DecorationCorner(),
    this.bottomLeft = const DecorationCorner(),
    this.bottomRight = const DecorationCorner(),
  });

  /// Creates corners with the same style and radius.
  const DecorationCorners.all(DecorationCorner corner)
      : topLeft = corner,
        topRight = corner,
        bottomLeft = corner,
        bottomRight = corner;

  /// Creates corners from a [BorderRadius] with a uniform style.
  factory DecorationCorners.fromBorderRadius(
    BorderRadius borderRadius, {
    DecorationCornerStyle style = DecorationCornerStyle.rounded,
  });

  final DecorationCorner topLeft;
  final DecorationCorner topRight;
  final DecorationCorner bottomLeft;
  final DecorationCorner bottomRight;

  /// Linearly interpolate between two corner sets.
  static DecorationCorners? lerp(DecorationCorners? a, DecorationCorners? b, double t);

  // ... equality, hashCode, etc.
}

1.4 DecorationShadow Class

/// A shadow specification that supports inner shadows.
@immutable
class DecorationShadow {
  const DecorationShadow({
    this.color = const Color(0x33000000),
    this.offset = Offset.zero,
    this.blurRadius = 0.0,
    this.spreadRadius = 0.0,
    this.inset = false,
  });

  final Color color;
  final Offset offset;
  final double blurRadius;
  final double spreadRadius;

  /// If true, the shadow is drawn inside the shape (inner shadow).
  final bool inset;

  /// Converts to a [BoxShadow] (for outer shadows only).
  BoxShadow toBoxShadow();

  /// Linearly interpolate between two shadows.
  static DecorationShadow? lerp(DecorationShadow? a, DecorationShadow? b, double t);

  /// Linearly interpolate between two shadow lists.
  static List<DecorationShadow> lerpList(
    List<DecorationShadow>? a,
    List<DecorationShadow>? b,
    double t,
  );
}

1.5 DecorationShapeBorder Class

/// A [ShapeBorder] that carries decoration data for rendering by [Material].
///
/// When used as the `shape` of a [Material] widget, the decoration properties
/// (gradient, shadows, image) will be rendered by the Material.
///
/// Example:
/// ```dart
/// Card(
///   shape: DecorationShapeBorder(
///     corners: DecorationCorners.all(
///       DecorationCorner(style: DecorationCornerStyle.superellipse, radius: Radius.circular(16)),
///     ),
///     gradient: LinearGradient(colors: [Colors.blue, Colors.purple]),
///     shadows: [
///       DecorationShadow(color: Colors.blue.withOpacity(0.3), blurRadius: 12, offset: Offset(0, 6)),
///     ],
///   ),
///   child: content,
/// )
/// ```
class DecorationShapeBorder extends OutlinedBorder {
  const DecorationShapeBorder({
    this.corners = const DecorationCorners(),
    super.side,
    this.color,
    this.gradient,
    this.shadows,
    this.image,
  }) : assert(color == null || gradient == null,
           'Cannot provide both a color and a gradient');

  /// The corners of this shape.
  final DecorationCorners corners;

  /// The color to fill the shape with.
  ///
  /// If [gradient] is also specified, [gradient] takes precedence.
  final Color? color;

  /// The gradient to fill the shape with.
  final Gradient? gradient;

  /// The shadows to paint around (and optionally inside) the shape.
  final List<DecorationShadow>? shadows;

  /// An image to paint inside the shape.
  final DecorationImage? image;

  /// Whether this border has decoration data that should be rendered.
  bool get hasDecoration => color != null || gradient != null ||
      (shadows != null && shadows!.isNotEmpty) || image != null;

  @override
  Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
    return _DecorationPathBuilder.build(rect, corners);
  }

  @override
  Path getInnerPath(Rect rect, {TextDirection? textDirection}) {
    return _DecorationPathBuilder.build(
      rect.deflate(side.strokeInset),
      corners,
    );
  }

  @override
  void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {
    // Only paints the border stroke - fill is handled by Material
    if (side != BorderSide.none) {
      final path = getOuterPath(rect, textDirection: textDirection);
      canvas.drawPath(path, side.toPaint());
    }
  }

  @override
  DecorationShapeBorder copyWith({
    BorderSide? side,
    DecorationCorners? corners,
    Color? color,
    Gradient? gradient,
    List<DecorationShadow>? shadows,
    DecorationImage? image,
  });

  @override
  ShapeBorder? lerpFrom(ShapeBorder? a, double t);

  @override
  ShapeBorder? lerpTo(ShapeBorder? b, double t);

  @override
  ShapeBorder scale(double t);
}

2. Modifications to Material Widget

The Material widget (and its internal _MaterialInterior) needs to detect when its shape is a DecorationShapeBorder and render the decoration accordingly.

2.1 Detection Mechanism

Add an interface that DecorationShapeBorder implements:

/// Interface for [ShapeBorder]s that provide decoration data.
abstract class DecorationProvider {
  /// The color to fill the shape with.
  Color? get decorationColor;

  /// The gradient to fill the shape with.
  Gradient? get decorationGradient;

  /// The shadows to paint.
  List<DecorationShadow>? get decorationShadows;

  /// An image to paint inside the shape.
  DecorationImage? get decorationImage;

  /// Whether this provider has any decoration data.
  bool get hasDecoration;
}

2.2 Material Widget Changes

In _MaterialInterior and related render objects:

// Pseudocode for Material's build method
Widget build(BuildContext context) {
  final shape = widget.shape;

  // Check if shape provides decoration data
  if (shape is DecorationProvider && shape.hasDecoration) {
    // Use decoration-aware rendering
    return _DecorationMaterial(
      shape: shape,
      decorationProvider: shape as DecorationProvider,
      clipBehavior: widget.clipBehavior,
      child: contents,
    );
  }

  // Existing Material rendering path (backward compatible)
  return _MaterialInterior(
    shape: shape,
    elevation: widget.elevation,
    color: backgroundColor,
    // ... existing implementation
  );
}

2.3 Decoration Rendering

The _DecorationMaterial widget would:

  1. Paint outer shadows - Non-inset DecorationShadow items
  2. Paint fill - Using color or gradient
  3. Paint image - Clipped to shape path
  4. Paint inner shadows - Inset DecorationShadow items (using path subtraction technique)
  5. Paint border - Using side property
  6. Render child - With proper clipping
class _DecorationMaterialPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final rect = Offset.zero & size;
    final path = shape.getOuterPath(rect);

    // 1. Outer shadows
    for (final shadow in shadows.where((s) => !s.inset)) {
      _paintOuterShadow(canvas, path, shadow);
    }

    // 2. Fill (color or gradient)
    if (gradient != null) {
      final paint = Paint()..shader = gradient!.createShader(rect);
      canvas.drawPath(path, paint);
    } else if (color != null) {
      canvas.drawPath(path, Paint()..color = color!);
    }

    // 3. Image (clipped)
    if (image != null) {
      canvas.save();
      canvas.clipPath(path);
      _paintImage(canvas, rect);
      canvas.restore();
    }

    // 4. Inner shadows
    if (shadows.any((s) => s.inset)) {
      canvas.save();
      canvas.clipPath(path);
      for (final shadow in shadows.where((s) => s.inset)) {
        _paintInnerShadow(canvas, rect, path, shadow);
      }
      canvas.restore();
    }
  }

  void _paintInnerShadow(Canvas canvas, Rect rect, Path path, DecorationShadow shadow) {
    // Inner shadow technique: paint shadow on inverted path
    final outerRect = rect.inflate(shadow.blurRadius * 2 + shadow.spreadRadius.abs());
    final outerPath = Path()..addRect(outerRect);
    final innerPath = path.shift(shadow.offset);
    // Shrink for spread (inner shadows shrink with positive spread)
    final shadowPath = Path.combine(PathOperation.difference, outerPath, innerPath);

    final paint = Paint()
      ..color = shadow.color
      ..maskFilter = MaskFilter.blur(BlurStyle.normal, shadow.blurRadius);

    canvas.drawPath(shadowPath, paint);
  }
}

3. Theme Integration

3.1 Component Themes

The DecorationShapeBorder can be used directly in component themes:

ThemeData(
  cardTheme: CardTheme(
    shape: DecorationShapeBorder(
      corners: DecorationCorners.all(
        DecorationCorner(style: DecorationCornerStyle.superellipse, radius: Radius.circular(16)),
      ),
      gradient: LinearGradient(
        colors: [Colors.blue.shade50, Colors.blue.shade100],
      ),
      shadows: [
        DecorationShadow(
          color: Colors.blue.withOpacity(0.2),
          blurRadius: 20,
          offset: Offset(0, 10),
        ),
      ],
    ),
  ),
  dialogTheme: DialogTheme(
    shape: DecorationShapeBorder(
      corners: DecorationCorners.all(
        DecorationCorner(style: DecorationCornerStyle.rounded, radius: Radius.circular(28)),
      ),
      shadows: [
        DecorationShadow(color: Colors.black26, blurRadius: 24, offset: Offset(0, 8)),
        DecorationShadow(color: Colors.white24, blurRadius: 4, inset: true), // Inner highlight
      ],
    ),
  ),
)

4. Animation Support

The DecorationShapeBorder fully supports animation through standard lerpFrom/lerpTo:

@override
ShapeBorder? lerpTo(ShapeBorder? b, double t) {
  if (b is DecorationShapeBorder) {
    return DecorationShapeBorder(
      corners: DecorationCorners.lerp(corners, b.corners, t)!,
      side: BorderSide.lerp(side, b.side, t),
      color: Color.lerp(color, b.color, t),
      gradient: Gradient.lerp(gradient, b.gradient, t),
      shadows: DecorationShadow.lerpList(shadows, b.shadows, t),
      // image lerping could transition opacity
    );
  }
  return super.lerpTo(b, t);
}

This enables smooth transitions when:

  • Widget state changes (hover, pressed, focus)
  • Theme changes
  • Explicit animations

API Impact Analysis

Breaking Changes: None

This proposal introduces no breaking changes:

  • Existing ShapeBorder implementations continue working unchanged
  • Material widget maintains backward compatibility
  • All existing theme configurations remain valid

New Public API

New classes in painting library:

  • DecorationCornerStyle (enum)
  • DecorationCorner
  • DecorationCorners
  • DecorationShadow
  • DecorationShapeBorder
  • DecorationProvider (interface)

Modified Classes

Material widget:

  • Internal detection of DecorationProvider shapes
  • New rendering path for decoration-aware shapes

Benefits

  1. Unified Styling API - Use the same shape property for both geometry and decoration
  2. Theme Integration - Decoration can be specified in ThemeData component themes
  3. Declarative - No builder functions required, just properties
  4. Backward Compatible - Existing code continues working
  5. Full Material Behavior - Ink splash, focus, accessibility all preserved
  6. Animation Support - Smooth transitions between states and themes
  7. Mixed Corner Types - Per-corner style and radius
  8. Inner Shadows - Enable neumorphic and modern UI patterns

Alternatives Considered

1. backgroundBuilder Pattern (Current Approach for Buttons)

Flutter 3.22 added backgroundBuilder to ButtonStyle. This could be extended to other components.

Pros:

  • Already implemented for buttons
  • Maximum flexibility

Cons:

  • Requires builder function (not declarative)
  • Cannot be used in static theme definitions
  • Must be implemented per-component
  • Different API pattern than shape property

2. New decoration Property on Material Components

Add a separate decoration property to Card, Dialog, etc.

Cons:

  • Adds another property (API bloat)
  • Unclear interaction with shape and color properties
  • Would need separate property in each component theme

3. ShapeDecoration Integration

Use existing ShapeDecoration class more directly.

Cons:

  • ShapeDecoration is a Decoration, not a ShapeBorder
  • Cannot be assigned to shape properties
  • Would require different API

Implementation Phases

Phase 1: Core Classes

  • Implement DecorationCorner, DecorationCorners, DecorationShadow
  • Implement DecorationShapeBorder
  • Add DecorationProvider interface
  • Unit tests for lerping and path generation

Phase 2: Material Integration

  • Modify Material widget to detect and render DecorationProvider shapes
  • Implement decoration painting (gradients, shadows, images)
  • Inner shadow rendering
  • Integration tests

Phase 3: Documentation and Examples

  • API documentation
  • Migration guide
  • Example app demonstrating usage with various components
  • Performance benchmarks

Related Issues

  • #89563 - ElevatedButton filled with gradient
  • #139456 - Allow arbitrary foreground and background elements for ButtonStyle
  • #130335 - Add Decoration to ButtonStyleButton's styling
  • #19147 - shape: RoundedRectangleBorder + Border.all -> Not Round
  • #104201 - Request: Add a way to round inside shape border

Summary

This proposal enables Flutter developers to use rich decoration styling (gradients, shadows, images) on Material components through the existing shape property, with full theme integration, animation support, and backward compa

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment