Custom Shapes with ShapeBorder in Flutter: Superellipses, Decorations, and Material 3
Write a ShapeBorder subclass with getOuterPath, getInnerPath, and lerp, then use it in ShapeDecoration, RoundedSuperellipseBorder, and TextField borders.
Published on • November 10, 2026
AI Assistant

Container’s BoxDecoration only offers rectangles, circles, and rounded rectangles, so anything more elaborate ends up in a CustomPainter that no theme can reach. ShapeBorder is the abstraction that fixes that: implement it once and the same geometry works in ShapeDecoration, as a Material shape, in a Card or Dialog theme, and even as a TextField border.
The ShapeBorder contract
The base class (abstract, @immutable) defines what your subclass must satisfy:
getOuterPath(Rect rect, {TextDirection? textDirection})- the outer edge, which must not cross the path fromgetInnerPath.getInnerPath(...)- the inner edge; combine it with the outer path underPathFillType.evenOddto describe the border band itself.paint(Canvas canvas, Rect rect, ...)- stroke the border;dimensionsgives the side widths asEdgeInsetsfor layout.scale(double t)pluslerpFrom/lerpTo(and staticShapeBorder.lerp) - what makes animated transitions work; returnnullfor shapes you cannot interpolate into and the framework falls back gracefully.preferPaintInterior/paintInterior- an optional fast path for filling the shape.
Subclass OutlinedBorder rather than ShapeBorder when your shape has a visible BorderSide: it adds side and copyWith, and its built-in siblings - RoundedRectangleBorder, CircleBorder, ContinuousRectangleBorder, BeveledRectangleBorder - are the reference implementations.
Writing a ShapeBorder subclass
class OctagonBorder extends OutlinedBorder {
const OctagonBorder({super.side = BorderSide.none, this.cut = 16});
final double cut;
Path _octagon(Rect rect) => Path()
..moveTo(rect.left + cut, rect.top)
..lineTo(rect.right - cut, rect.top)
..lineTo(rect.right, rect.top + cut)
..lineTo(rect.right, rect.bottom - cut)
..lineTo(rect.right - cut, rect.bottom)
..lineTo(rect.left + cut, rect.bottom)
..lineTo(rect.left, rect.bottom - cut)
..lineTo(rect.left, rect.top + cut)
..close();
@override
Path getOuterPath(Rect rect, {TextDirection? textDirection}) => _octagon(rect);
@override
Path getInnerPath(Rect rect, {TextDirection? textDirection}) =>
_octagon(rect.deflate(side.strokeInset));
@override
void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {
if (side.style == BorderStyle.none) return;
canvas.drawPath(_octagon(rect), side.toPaint());
}
@override
ShapeBorder? lerpTo(ShapeBorder? b, double t) => b is OctagonBorder
? OctagonBorder(
side: BorderSide.lerp(side, b.side, t),
cut: cut + (b.cut - cut) * t,
)
: super.lerpTo(b, t);
}
ShapeDecoration and DecoratedBox
Unlike BoxDecoration, ShapeDecoration accepts any ShapeBorder and can fill the interior, stroke it, and clip the child to getOuterPath:
DecoratedBox(
decoration: ShapeDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
shape: const OctagonBorder(side: BorderSide(width: 2), cut: 24),
),
child: const Padding(padding: EdgeInsets.all(24), child: Text('Octagonal tile')),
)
RoundedSuperellipseBorder: the iOS squircle
RoundedSuperellipseBorder follows an RSuperellipse instead of an RRect, reproducing the smoother “squircle” iOS uses. It landed in stable Flutter 3.32 alongside ClipRSuperellipse and Canvas.drawRSuperellipse, and Cupertino’s alert dialog and action sheet now use it:
ShapeDecoration(
shape: RoundedSuperellipseBorder(
borderRadius: BorderRadius.circular(20),
),
)
The API mirrors RoundedRectangleBorder (side, borderRadius, copyWith, getInnerPath, paintInterior), so switching is a one-line change. The 3.32 announcement notes rounded superellipses were initially supported only on iOS and Android, with other platforms falling back to a standard rounded rectangle.
Material 3: cards, dialogs, and text fields
Material 3 shapes are per-component theme data rather than one global token. Card.shape falls back to CardThemeData.shape, then to a RoundedRectangleBorder with a radius of 12.0 under Material 3 (4.0 otherwise); Dialog.shape defaults to radius 4.0:
ThemeData(
useMaterial3: true,
cardTheme: CardThemeData(
shape: const OctagonBorder(side: BorderSide(width: 1), cut: 18),
),
)
For TextField, InputDecoration.border is an InputBorder (itself a ShapeBorder), and the decorator overrides its borderSide to reflect focus, error, and disabled state — only your shape is used. To feed an arbitrary shape in while keeping the floating-label gap, wrap it in ShapedInputBorder:
TextField(
decoration: InputDecoration(
labelText: 'Octagonal field',
border: ShapedInputBorder(
shape: const OctagonBorder(side: BorderSide(width: 1.5), cut: 14),
gapPadding: 6,
),
),
)
State-dependent outlines can also use WidgetStateInputBorder, which resolves per WidgetState, and OutlineInputBorder remains the rounded-rectangle default.
Performance: paintInterior and preferPaintInterior
Filling a shape normally means Canvas.drawPath(getOuterPath(...)). When the canvas has a dedicated primitive for your shape (drawRRect, drawCircle, drawRSuperellipse), override paintInterior and return preferPaintInterior => true; the docs state this beats drawPath, and that ShapeDecoration caches the outer path when you cannot provide one. The rule is strict: paintInterior must produce exactly the pixels drawPath would, and a path-only shape must leave preferPaintInterior false.
Takeaway: Implement getOuterPath, getInnerPath, paint, and lerpTo on an OutlinedBorder subclass to make a shape first-class, drop it into ShapeDecoration, Material, or cardTheme, reach for RoundedSuperellipseBorder for iOS-style squircles, and wrap your shape in ShapedInputBorder to give TextField the same outline.
Sources
- https://api.flutter.dev/flutter/painting/ShapeBorder-class.html
- https://api.flutter.dev/flutter/painting/RoundedSuperellipseBorder-class.html
- https://api.flutter.dev/flutter/material/ShapedInputBorder-class.html
- https://api.flutter.dev/flutter/material/InputDecoration/border.html
- https://api.flutter.dev/flutter/material/Card/shape.html
- https://flutter.dev/blog/whats-new-in-flutter-3-32
- https://docs.flutter.dev/ui/design