4.5 KiB
4.5 KiB
Shape System - M3 Expressive
Overview
Material 3 Expressive introduces a dynamic shape system that moves beyond simple rounded rectangles. Shapes are used as containers, buttons, and state indicators, focusing on fluidity and hierarchy.
Design Principles
- Expressive Shapes: Use non-standard shapes (pixel triangles, teardrops, squircles) to convey brand personality.
- Shape Hierarchy: Larger, more complex shapes denote higher importance.
- Fluid Morphing: Shapes should seamlessly transition between states (e.g., FAB to Extended FAB).
- Containment: Use shapes to group related content clearly.
Corner Token System
M3 uses a tokenized corner radius system:
| Token | Radius | Example Usage |
|---|---|---|
none |
0dp | Full-width elements, square buttons |
extra-small |
4dp | Tooltips, selection controls |
small |
8dp | Chips, snackbars |
medium |
12dp | Cards, small dialogs |
large |
16dp | Extended FABs, menus |
extra-large |
28dp | FABs, navigation rails/bars |
full |
100% | Circular buttons, pill-shaped tags |
Implementation in Flutter
1. Rounded Rectangle (Standard M3)
ElevatedButton(
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16), // Large token
),
),
onPressed: () {},
child: const Text('Standard M3'),
)
2. Squircle / Continuous Corners
For a more premium feel, use "continuous" corners (similar to iOS) instead of standard circular arcs.
// Use a package or custom clipper for true squircles
Container(
decoration: ShapeDecoration(
shape: SmoothRectangleBorder(
borderRadius: BorderRadius.circular(24),
smoothness: 1, // Full squircle
),
color: Colors.blue,
),
child: const Padding(
padding: EdgeInsets.all(16),
child: Text('Squircle Container'),
),
)
3. Teardrop Shape (Expressive)
Commonly used for active indicators or expressive buttons.
class TeardropShape extends ShapeBorder {
@override
EdgeInsetsGeometry get dimensions => EdgeInsets.zero;
@override
Path getInnerPath(Rect rect, {TextDirection? textDirection}) => getOuterPath(rect);
@override
Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
return Path()
..moveTo(rect.left + rect.width / 2, rect.top)
..arcToPoint(
Offset(rect.right, rect.top + rect.height / 2),
radius: Radius.circular(rect.width / 2),
)
..lineTo(rect.right, rect.bottom)
..lineTo(rect.left + rect.width / 2, rect.bottom)
..arcToPoint(
Offset(rect.left, rect.top + rect.height / 2),
radius: Radius.circular(rect.width / 2),
)
..close();
}
@override
void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {}
@override
ShapeBorder scale(double t) => this;
}
4. Shape Morphing Animation
class MorphingShapeButton extends StatefulWidget {
const MorphingShapeButton({super.key});
@override
State<MorphingShapeButton> createState() => _MorphingShapeButtonState();
}
class _MorphingShapeButtonState extends State<MorphingShapeButton> {
bool _isRound = true;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => setState(() => _isRound = !_isRound),
child: AnimatedContainer(
duration: const Duration(milliseconds: 500),
curve: Curves.elasticOut,
width: 100,
height: 100,
decoration: BoxDecoration(
color: Colors.deepPurple,
borderRadius: BorderRadius.circular(_isRound ? 50 : 8),
),
child: const Center(
child: Icon(Icons.refresh, color: Colors.white),
),
),
);
}
}
Best Practices
- Consistency: Stick to a "shape family" across your app. If you use squircles, use them everywhere.
- Accessible Targets: Ensure that even with expressive shapes, the touch target remains at least 48x48dp.
- Contrast: Shapes should be distinguishible from their background; use subtle shadows or outlines.
- Performance: Complex custom paths in
CustomPaintershould be optimized. Useconstpaths where possible.
Resources
Related
- Motion Guide for shape transitions
- Color Schemes for container colors