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.
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 (
paintmethod) - 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.
Developers currently resort to:
-
Wrapper stacks - Wrapping components in
StackwithDecoratedBox:Stack( children: [ DecoratedBox(decoration: myGradientDecoration), Card(color: Colors.transparent, child: content), ], )
- Breaks widget tree semantics
- Complicates layout
- Doesn't integrate with theming
-
Custom widget implementations - Recreating Material components:
- Loses Material behaviors (ink splash, focus handling, accessibility)
- Significant development overhead
- Diverges from framework updates
-
ButtonStyle.backgroundBuilder(Flutter 3.22+):- Only works for button components
- Requires builder function instead of declarative style
- Cannot be used in
ThemeDatacomponent themes
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
Introduce a decoration-aware ShapeBorder system that:
- Creates a new
OutlinedBordersubclass (DecorationShapeBorder) that carries decoration data (gradient, shadows, image, color) - Modifies
Materialwidget to detect and render decoration data from itsshapeproperty - Maintains full backward compatibility - existing
ShapeBorderimplementations continue working unchanged - Enables theming - decoration can be specified in
ThemeDatacomponent themes
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
/// 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,
}/// 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.
}/// 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.
}/// 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,
);
}/// 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);
}The Material widget (and its internal _MaterialInterior) needs to detect when its shape is a DecorationShapeBorder and render the decoration accordingly.
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;
}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
);
}The _DecorationMaterial widget would:
- Paint outer shadows - Non-inset
DecorationShadowitems - Paint fill - Using
colororgradient - Paint image - Clipped to shape path
- Paint inner shadows - Inset
DecorationShadowitems (using path subtraction technique) - Paint border - Using
sideproperty - 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);
}
}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
],
),
),
)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
This proposal introduces no breaking changes:
- Existing
ShapeBorderimplementations continue working unchanged Materialwidget maintains backward compatibility- All existing theme configurations remain valid
New classes in painting library:
DecorationCornerStyle(enum)DecorationCornerDecorationCornersDecorationShadowDecorationShapeBorderDecorationProvider(interface)
Material widget:
- Internal detection of
DecorationProvidershapes - New rendering path for decoration-aware shapes
- Unified Styling API - Use the same
shapeproperty for both geometry and decoration - Theme Integration - Decoration can be specified in
ThemeDatacomponent themes - Declarative - No builder functions required, just properties
- Backward Compatible - Existing code continues working
- Full Material Behavior - Ink splash, focus, accessibility all preserved
- Animation Support - Smooth transitions between states and themes
- Mixed Corner Types - Per-corner style and radius
- Inner Shadows - Enable neumorphic and modern UI patterns
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
shapeproperty
Add a separate decoration property to Card, Dialog, etc.
Cons:
- Adds another property (API bloat)
- Unclear interaction with
shapeandcolorproperties - Would need separate property in each component theme
Use existing ShapeDecoration class more directly.
Cons:
ShapeDecorationis aDecoration, not aShapeBorder- Cannot be assigned to
shapeproperties - Would require different API
- Implement
DecorationCorner,DecorationCorners,DecorationShadow - Implement
DecorationShapeBorder - Add
DecorationProviderinterface - Unit tests for lerping and path generation
- Modify
Materialwidget to detect and renderDecorationProvidershapes - Implement decoration painting (gradients, shadows, images)
- Inner shadow rendering
- Integration tests
- API documentation
- Migration guide
- Example app demonstrating usage with various components
- Performance benchmarks
- #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
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