material_design 1.0.1
material_design: ^1.0.1 copied to clipboard
A Material Design 3 contract for Flutter. Enforces M3 tokens system-wide, replacing free primitives with type-safe M3 equivalents.
Material Design 3 for Flutter #
A Material Design 3 design contract for Flutter. Instead of documenting the M3
spec and hoping everyone follows it, this package expresses the spec as Dart types:
APIs take M3SpacingValue instead of double, so an off-scale value is a compile
error, not a design-review comment.
- π Live demo β every token, interactive
- π example/lib/main.dart β the whole API in one copy-pasteable file
- π Official M3 guidelines
Why #
| Plain Flutter (anything compiles) | With the contract (only M3 compiles) |
|---|---|
EdgeInsets.all(17.3) |
const M3EdgeInsets.all(M3Spacings.s16) |
SizedBox(height: 14) |
const M3Gap(M3Spacings.s12) |
BorderRadius.circular(15) |
M3BorderRadius.medium β 12dp |
BoxShadow(blurRadius: 4) |
M3ElevationShadows.level2 |
Opacity(opacity: 0.35) |
M3Opacities.disabledContent β 38% |
TextStyle(fontSize: 15) |
M3TypeScale.bodyLarge β 16sp, 24 height |
Duration(milliseconds: 280) + guessed curve |
M3Motion.standard β 300ms + standard easing |
| hand-built focus border | M3FocusRing(child: β¦) β official 3dp ring, 3dp offset |
Everything is const, extension types are erased at compile time, and unused code
tree-shakes away β the contract costs nothing at runtime.
Install #
dependencies:
material_design: ^1.0.0
Requires Flutter >=3.27.0 / Dart >=3.6.0 (uses Color.withValues and extension
types).
Quick start #
import 'package:flutter/material.dart';
import 'package:material_design/material_design.dart';
MaterialApp(
// Merges the M3 type scale into your theme without losing its colors.
theme: M3TextTheme.applyToTheme(
ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF6750A4)),
),
),
home: const HomePage(),
);
Then build with tokens instead of numbers:
Card(
shape: M3Shape.medium, // 12dp corners
child: M3Padding(
padding: const M3EdgeInsets.all(M3Spacings.s16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Title', style: M3TypeScale.titleMedium),
const M3Gap(M3Spacings.s8), // knows it's inside a Column
Text('Body', style: M3TypeScale.bodyMedium),
],
),
),
);
API tour #
Each family below has a page in the live demo and a section in example/lib/main.dart, in this same order.
1. Spacing & layout #
M3Spacingsβ the 4dp grid:none,s4β¦s128,infinity, plusM3Spacings.valuesfor galleries.M3Margins(16dp compact / 24dp elsewhere) andM3Spacers.pane(24dp).M3EdgeInsetsβall,symmetric,only,fromLTRB, every parameter anM3SpacingValue.M3EdgeInsetsPatternsships common recipes (card,dialog,listItem,compactPage,expandedPage).M3PaddingβPaddingthat only acceptsM3EdgeInsets.M3Gapβ a spacer that detects whether it sits in aRow,Column,Wrap, or scrollable and orients itself.M3GapUtils.addGaps(children, M3Spacings.s8)interleaves gaps into an existing list.
Column(
children: [
const Text('Header'),
const M3Gap(M3Spacings.s16), // vertical here, horizontal in a Row
const Text('Body'),
],
);
2. Shape & borders #
The M3 shape scale has exactly seven stops: none 0 Β· extraSmall 4 Β· small 8 Β·
medium 12 Β· large 16 Β· extraLarge 28 Β· full (pill). Each shape type extends
its Flutter counterpart, so they drop in anywhere:
Card(shape: M3Shape.medium); // RoundedRectangleBorder
Container(
decoration: M3BoxDecoration(
borderRadius: M3BorderRadius.large, // BorderRadius
border: M3Border.thin(colorScheme.outline), // Border, 1dp
),
);
const M3BorderRadius.only(topLeft: M3Radius.large, topRight: M3Radius.none);
Border widths are their own scale β M3BorderWidths: none 0, thin 1, thick 2,
extraThick 4 β with named constructors M3BorderSide.thin/thick/extraThick(color).
3. Elevation & surfaces #
M3Elevation is a composite token: each level carries its dp and its shadows.
M3Elevation.level2.dp; // 3.0
M3Elevation.level2.shadows; // ready-made shadow list
M3Elevation.level2.surfaceColor(context); // surface blended with tint at 3dp
colorScheme.surfaceAtElevation(M3Elevation.level2); // same, from the scheme
Levels: 0, 1, 3, 6, 8, 12 dp. Static shadow lists: M3ElevationShadows.level0β¦5.
M3ShapeDecoration pairs an M3Shape with those shadows for Container.decoration.
4. Typography #
The 15 M3 styles as const TextStyles with exact spec metrics:
Text('Section', style: M3TypeScale.headlineSmall);
Text('Body', style: M3TypeScale.bodyMedium);
M3TextTheme.applyToTheme(theme) merges them into a ThemeData (see Quick start).
M3TextUtils covers the runtime cases: clampedScaler (bounded text scaling β
last resort, it fights the user's accessibility setting), responsiveDisplay,
dyslexiaFriendly, mono, highContrast, withFontFamily.
5. Color #
Real HCT tonal palettes β the same math Material uses, via
material_color_utilities:
final palette = M3TonalPalette.fromSeed(const Color(0xFF6750A4));
palette[M3Tones.t40]; // light-scheme primary
palette[M3Tones.t80]; // dark-scheme primary
final core = M3CorePalette.fromSeed(seed);
core.neutral[M3Tones.t99]; // light surface
core.error[M3Tones.t40]; // spec red, independent of seed
On ColorScheme: stateLayerColor(base, M3InteractionState.hover),
disabledContent(base) (38%), disabledContainer(base) (12%),
surfaceAtElevation(level), isAccessible(fg, bg).
M3ColorUtils adds WCAG contrast math (calculateContrast, meetsWCAGAA/AAA,
adjustForAccessibility) and color manipulation.
Opacity tokens: M3Opacities (38/12/12/50%) and M3StateLayerOpacities
(hover 8%, focus 10%, pressed 10%, dragged 16%).
6. Interaction states & focus #
M3StateLayer( // hover/focus/press/drag overlays, M3 precedence
overlayColor: colorScheme.onSurface,
borderRadius: M3BorderRadius.medium,
onTap: () {},
child: content,
);
M3FocusRing( // official 3dp ring at 3dp offset
borderRadius: M3BorderRadius.full,
child: IconButton(onPressed: () {}, icon: const Icon(Icons.star)),
);
M3FocusRing reserves its 6dp inset even when unfocused, so tabbing never shifts
the control; it observes focus and never takes it.
M3VisualDensity provides standard/comfortable/compact,
forPlatform(platform) and forScreenSize(size).
7. Motion #
Duration and curve travel together β you never pair them by hand:
AnimatedContainer(
duration: M3Motion.emphasized.duration, // 500ms
curve: M3Motion.emphasized.curve, // emphasized easing
);
Schemes: emphasized, emphasizedIncoming, emphasizedOutgoing, standard,
standardIncoming, standardOutgoing, linear. For const contexts use the flat
aliases (M3Motion.emphasizedDuration). Pick by intent with
M3Motion.durationFor(M3MotionDistance.long) /
M3Motion.curveFor(M3MotionType.incoming). Raw scales: M3MotionDuration.short1β¦extraLong4
(50β1000ms), M3MotionCurve.* (the official cubics).
8. Adaptive & responsive #
M3ScreenSize is the M3 window-size-class selector, and everything else keys off it:
final size = M3ScreenSize.of(context); // compact / medium / expanded / large / extraLarge
size.columns; // 4 / 8 / 12
size.isAtLeast(M3ScreenSize.medium);
M3ResponsiveValue<int>(compact: 2, medium: 4, expanded: 6,
builder: (context, cols) => grid(cols));
M3ResponsiveVisibility(visibleOn: const [M3ScreenSize.expanded], child: sidebar);
M3ResponsiveScaffold(destinations: β¦); // bottom bar β rail β drawer, automatically
Breakpoints (M3Breakpoints: 0/600/840/1200/1600) are tokens too. M3Adaptive
bundles static helpers: responsiveLayout, adaptivePadding, adaptiveNavigation,
showAdaptiveDialog (fullscreen on phones, dialog on desktop), showAdaptiveSheet
(bottom sheet β side panel), adaptiveButton (48dp touch / 32dp mouse targets).
9. Accessibility #
M3Accessibility.minTouchTarget(context); // 48dp touch, 32dp desktop
M3Accessibility.meetsContrastRequirement(foreground: fg, background: bg);
M3Accessibility.shouldReduceMotion(context);
M3Accessibility.adaptiveDuration(context: context, normal: d); // honors reduce-motion
M3AccessibilityConfig.fromContext(context).applyToTheme(theme) adapts a whole
theme to the user's contrast/motion/text-size settings.
10. M3 Expressive (experimental) #
The 2025 M3 Expressive primitives, in the m3e module:
M3ELoadingIndicator(+.contained()) β the morphing loading indicator that replaces most indeterminate spinners.MaterialShapesβ the official 35-shape library (circleβ¦heart) asRoundedPolygons, plusMorphfor shape-to-shape animation andtoPath()to draw them.
β οΈ The shape engine currently exports unprefixed names (
Point,Cubic,Morph,lerp, β¦). If they collide with your imports, useimport 'package:material_design/material_design.dart' hide Point;β a scopedM3Enamespace is planned.
Breaking the contract, deliberately #
Sometimes the design system is not the authority β a brand asset really is 18dp.
M3Contract is the one sanctioned way out:
M3EdgeInsets.all(M3Contract.spacing(18)) // off the 4dp grid, on purpose
Factories exist for every scale: spacing, corner, borderWidth, opacity,
iconSize, breakpoint, elevationDp, zIndex. Because every deviation names the
same identifier, compliance is measurable:
grep -rn 'M3Contract\.' lib/ | wc -l # how far has this app drifted from M3?
Zero-drift teams fail CI on any hit; migrating teams watch the number fall. Either
way deviations are visible β which beats an "unbreakable" contract with a silent
as-cast in it. (Extension types are erased at runtime, so a cast always compiles;
the package is honest about that instead of pretending otherwise.)
The rules behind the API #
- Primitive replacement. Every scale is an extension type
(
M3SpacingValue,M3CornerValue, β¦) with a library-private constructor. No family quietly still takes a rawdouble. - No token that must be unwrapped. Scalar tokens are
static constvalues used directly β the oldM3SpacingToken.space16.valuepattern is gone; it costconst-ness at every call site. Enums remain only where they're right: composite tokens (M3Motion,M3Elevationβ two fields read together) and selectors (M3ScreenSize,M3InteractionStateβ they name a situation). - Deviation is explicit and greppable β
M3Contract, above.
Architecture #
Nine modules, one-directional dependencies; import 'package:material_design/material_design.dart'
gives you all of them:
tokens βββ¬β> shape βββ¬β> interaction
ββ> layout ββ€
ββ> color βββ΄β> adaptive
ββ> typography
motion βββββββββββββββββ> interaction, adaptive
expressive (standalone)
Showcase #
A card with hover overlays, keyboard focus, tokenized spacing and motion β compiled
and rendered by test/readme_showcase_test.dart, so this snippet cannot drift from
the API:
import 'package:flutter/material.dart';
import 'package:material_design/material_design.dart';
class PremiumCardShowcase extends StatelessWidget {
const PremiumCardShowcase({super.key});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return M3FocusRing(
borderRadius: M3BorderRadius.large,
child: M3StateLayer(
overlayColor: colorScheme.onSurface,
borderRadius: M3BorderRadius.large,
onTap: () {
// Action handler
},
child: AnimatedContainer(
duration: M3Motion.emphasized.duration,
curve: M3Motion.emphasized.curve,
padding: const M3EdgeInsets.all(M3Spacings.s24),
decoration: M3BoxDecoration(
color: colorScheme.surfaceAtElevation(M3Elevation.level1),
borderRadius: M3BorderRadius.large,
border: M3Border.all(
outlineColor: colorScheme.outlineVariant,
width: M3BorderWidths.thin,
),
boxShadow: M3ElevationShadows.level1,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'COMPILATION SAFE',
style: M3TypeScale.labelMedium.copyWith(
color: colorScheme.primary,
),
),
const M3Gap(M3Spacings.s8),
Text(
'Material 3 Contract Design',
style: M3TypeScale.titleLarge.copyWith(
color: colorScheme.onSurface,
),
),
const M3Gap(M3Spacings.s16),
Text(
'Every spacing, border width, opacity, and text style here '
'flows through an M3 token.',
style: M3TypeScale.bodyMedium.copyWith(
color: colorScheme.disabledContent(colorScheme.onSurface),
),
),
],
),
),
),
);
}
}
Versioning #
1.0.x is young and has no deprecation baggage: API corrections ship as renames
with a migration table in the CHANGELOG. Upcoming work (Expressive
spring motion tokens, color scheme variants, emphasized type scale) follows the
same contract rules.
License #
BSD 3-Clause β see LICENSE.