Material 3 Expressive
A faithful Flutter implementation of the Material 3 Expressive component set.
Every widget is exposed as a direct M3E* class with spring-driven press
feedback, shape morphing, and hover/focus/press state layers. Design tokens
(color, typography, motion, shapes, elevation) are provided through
M3ETheme.
Runtime dependencies are intentionally small — see
Dependencies for the packages declared in
pubspec.yaml.
Samples
| Actions | Selection |
|---|---|
![]() |
![]() |
| Containment | Navigation |
|---|---|
![]() |
![]() |
| Feedback |
|---|
![]() |
Example app
Try the live gallery on the web: paadevelopments.github.io/material_3_expressive.
An interactive gallery demonstrating all 44 widgets also lives in the
example/ directory (same build as the live demo). It groups
components the same way as the official Material 3 catalog, with a live
playground per component under example/lib/pages/playground/.
Each playground includes a Code section with paste-ready Dart that tracks
the current controls (copy to clipboard).
| Tab | Playgrounds | Components |
|---|---|---|
| Do | playground/do/ |
Buttons, FABs, FAB menu, groups, segmented & split buttons |
| Pick | playground/pick/ |
Checkbox, radio, switch, chips, dropdown, slider (incl. wavy), pickers |
| View | playground/view/ |
Cards, carousel, lists, selection, divider, dialogs, sheets, shapes, typography |
| Nav | playground/nav/ |
App bars (incl. search), tabs, nav bar/rail/drawer, toolbar, menu |
| Find | playground/find/ |
Badges, progress, refresh, tooltip, snackbar, inputs |
The gallery shell in example/lib/main.dart uses
M3EMaterialApp with adaptive theming, a light/dark toggle, and a palette
action that opens theme_config_page.dart
(auto theming, dynamic color, five seed colors, and font family — default
Google Sans Flex). For type scale, variable-font axes, and style
conversion, use the Typography playground under the View tab.
cd example
flutter run
Features
- 44 widgets across 39 component modules, covering Actions, Selection, Containment, Navigation, and Feedback (communication + text input).
- Direct component API — construct each
M3E*widget directly; enums and models are exported from a single library import. Action surfaces accept optional gradient decorations (fill, foreground, overlay, outline). - Expressive motion & interaction — spring physics (via
motor), shape morphing, per-destination selection indicators, shared haptics (M3EHaptics), and proper state layers on every interactive surface. - Design token foundations — color schemes, typography, motion, shapes
(including
material_new_shapesmorph polygons), spacing and radius viaM3EDimensions, elevation, haptics, and state layers via theM3EThemeinherited widget. - Interactive example gallery — run locally from
example/, or open the live web demo.
Requirements
| Tool | Version |
|---|---|
| Flutter | >= 3.47.0 |
| Dart | ^3.13.0 |
Migrating to material_ui
This package uses material_ui ^1.5.0
for Material widgets (MaterialApp, ThemeData, ColorScheme, and the rest of
the Material library). Do not import package:flutter/material.dart.
import 'package:material_ui/material_ui.dart';
import 'package:material_3_expressive/material_3_expressive.dart';
Apps that still import package:flutter/material.dart should switch those
imports to package:material_ui/material_ui.dart. Flutter 3.47.0 or newer
(Dart 3.13.0+) is required (material_ui will not resolve on older SDKs).
ColorScheme.harmonized() / Color.harmonizeWith() come from
dynamic_color ^2.1.0 (re-exported
through this package). Prefer those APIs rather than a local duplicate.
What's new in 1.1.4
Summary of updates since 1.1.3 (details in CHANGELOG.md):
- Deps —
material_ui^1.5.0. - Sliders — sizes
xs–xl(tracks 16–96, handles 44–108). Active track and handle are primary; inactive track is secondary container. Wavy tracks takewavelength/waveSpeed. Value indicator is 48×44, fully rounded, inverse surface. - Toolbars — docked and floating bars are 64 tall. Standard content
is surface container; vibrant is primary container.
contentAlignmentplaces docked actions at 600dp and wider. Scroll-exit does not run together with collapse-to-FAB. - Focus rings — hover, scroll, and press hide every keyboard focus ring as soon as the pointer interaction starts. Tab and the arrow keys show it again.
- Navigation drawer — standard sheet is surface; modal is surface
container. Width 360. A modal drawer dismisses on a destination,
the scrim, a drag toward the start edge, or system back.
M3ENavigationDrawerControllerselects, opens, and closes it, andsectionsgroup destinations below the primary list. - Navigation rail — collapsed width 96 (narrow 80), expanded
220–360. A modal rail dismisses on the scrim, Escape, or system back
and leaves no collapsed rail behind.
M3ENavigationRailControllerselects, expands, and shows the immersive rail. - Lists — expressive defaults (heights 56 / 72 / 88).
M3EListis the only list widget —M3ECardList,M3EDismissibleList,M3EDismissibleColumn, andM3EExpandableListare removed. A dragged row lifts to level 4 and commits when the drag ends. - App bars — small content band is 64; flexible medium 112 and
large 120 (taller with a subtitle).
M3EAppBarHideMode.entireslides the whole bar;.actionsleaves each action floating on its own fill.M3EAppBarController(expand,collapse,show,hide,followScroll) drives the bar. - Carousel — multi-browse, uncontained, uncontained multi-aspect, hero,
and full-screen. Items are
M3ECarouselItemvalues.M3ECarouselControllersteps withnext/previous, moves withanimateToItem/jumpToItem, and opens the list withshowAll. - Navigation bar — flexible size is 64, baseline 80 is the
default. The selected pill is opaque as soon as the destination changes.
M3ENavigationBarControllerselects a destination and, withhideOnScroll, shows or hides the bar from the page's scroll controller. - Tabs — primary label-only and secondary bars are 48; primary icon
plus label is 64.
M3ETabsControllerselects a tab,M3ETabsViewswipes between bodies, andM3ETabs.sliverscrolls away and returns on an upward scroll. - Tooltips — a plain tooltip stays off its anchor and ignores the pointer, so the cursor on an icon button does not twitch.
- Interaction — hover stays off while a scrollable is moving, so a fast fling does not flash the controls under the pointer.
- Cards —
verticalstacks media above the text. Swipe can dismiss, reveal a leading or trailing action, or do both.openBuilderruns a full-screen container transform.M3ECardGrouplays out a collection (grid, staggered, list, carousel) and reorders on a long-press drag.
Installation
Add the package to your pubspec.yaml:
dependencies:
material_3_expressive: ^1.1.4
Then fetch it:
flutter pub get
Or add it from the command line:
flutter pub add material_3_expressive
Dependencies
External packages declared in pubspec.yaml:
| Package | Role in this library |
|---|---|
flutter |
SDK — widgets, painting, gestures |
material_ui |
Official Material widget library (MaterialApp, ThemeData, ColorScheme) |
collection |
Small collection helpers used by component logic |
dynamic_color |
Platform dynamic / Material You seed colors for M3EMaterialApp (dynamicColoring); ColorScheme.harmonized / Color.harmonizeWith (2.x, material_ui) |
motor |
Unified motion API — physics springs and curves that drive expressive morphs and selection indicators |
material_new_shapes |
Expressive RoundedPolygon morph shapes (M3EMaterialNewShapes, M3EShapeKind, M3EShapeClipper, M3EShapeContainer) used by loading / shape-driven surfaces |
Dev-only: flutter_lints, flutter_test, and custom_lint.
Quick start
Import the library — a single import exposes every component and foundation:
import 'package:material_3_expressive/material_3_expressive.dart';
Recommended: M3EMaterialApp
Wrap your app in M3EMaterialApp for adaptive theming, dynamic color, and
Material ThemeMode alignment (same pattern as the example gallery):
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return M3EMaterialApp(
title: 'My App',
data: M3EThemeData.light(seedColor: const Color(0xFF6750A4)),
autoTheming: true,
dynamicColoring: true,
drawUnderSystemBars: true, // transparent system bars, edge-to-edge layout
home: const HomePage(),
);
}
}
Surfaces draw behind the OS navigation bar; components such as M3ENavigationBar
keep interactive content above the gesture area via viewPadding.
Actionable controls draw an outset keyboard focus ring when focused via
Tab (hidden after pointer interaction). Override globally with
focusRingTheme / keyboardFocusIndicators on M3EThemeData, or try the
example Focus rings playground (View tab):
M3EThemeData.light(seedColor: seed).copyWith(
keyboardFocusIndicators: true,
focusRingTheme: const M3EFocusRingTheme(
color: Color(0xFF6750A4),
width: 2,
gap: 2,
),
);
Alternative: M3ETheme subtree
If you already have an app shell, wrap any subtree in M3ETheme:
M3ETheme(
data: M3EThemeData.light(seedColor: const Color(0xFF6750A4)),
child: myApp,
);
Use M3EThemeData.dark(...) for a dark scheme. If no M3ETheme is found,
components fall back to a default light theme.
Accessing theme tokens
final theme = M3ETheme.of(context);
final scheme = theme.colorScheme;
final type = theme.typeScale;
// Toggle brightness at runtime (requires adaptive M3EMaterialApp / M3EThemeScope)
M3ETheme.controllerOf(context)?.toggleBrightness(
fallback: theme.brightness,
autoTheming: true,
);
Theming
M3EThemeData bundles expressive tokens and per-component themes:
final theme = M3EThemeData.light(
seedColor: const Color(0xFF6750A4),
);
// Override a single component theme
final custom = theme.copyWith(
buttonTheme: M3EButtonTheme.defaults.copyWith(/* ... */),
);
Key properties on M3EThemeData:
colorScheme—M3EColorSchemewith M3 semantic rolestypography—M3ETypographywith baseline and emphasized scales (30 styles)typeScale— baseline alias fortypography.baseline(used by components)spacing,visualDensity, per-component*ThemeextensionsfocusRingTheme/keyboardFocusIndicators— keyboard focus chrome- Per-component
M3ESpringmotion fields on themes such asswitchTheme,fabMenuTheme,navigationRailTheme,listTheme,toolbarTheme,sliderTheme,iconButtonTheme,checkboxTheme, andrefreshIndicatorTheme(defaults preserve prior hard-coded springs)
The M3 type system has 15 baseline and 15 emphasized roles. Use emphasized styles for selection, actions, and editorial hierarchy:
final theme = M3ETheme.of(context);
Text('Headline', style: theme.typography.emphasized.headlineSmall);
Shared typography (font family, fallback, package, size factor/delta, color,
decoration, brand/plain typefaces, and variable-font axes) is applied through
M3ETypography.apply or theme copyWith — not a full TextStyle:
final themed = M3EThemeData.light(seedColor: seed).copyWith(
typography: M3ETypography.material3().apply(
fontFamily: 'Roboto Flex',
fontVariations: M3ETypeVariations.graded.variations,
),
);
// Sugar: same result without building typography by hand
final alsoThemed = M3EThemeData.light(seedColor: seed).copyWith(
fontFamily: 'Roboto Flex',
fontVariations: M3ETypeVariations.graded.variations,
);
// Per-role variable-font axes (opsz, wght, split ROND, emphasized GRAD)
final variable = M3EThemeData.light(seedColor: seed).copyWith(
fontFamily: 'Roboto Flex',
typeScaleMode: M3ETypeScaleMode.variable,
variableFont: const M3EVariableFontConfig(
global: M3EVariableFontAxes(wght: 500, opsz: 16),
brand: M3EVariableFontAxes(rond: 25),
body: M3EVariableFontAxes(rond: 50),
),
);
// Convert any TextStyle to a spec variant
final converted = M3ETypeStyleConversion.toVariant(
Theme.of(context).textTheme.bodyLarge!,
variant: M3ETypeScaleVariant.emphasized,
role: M3ETypeRole.bodyLarge,
);
// Customize token fields before building a TextStyle
final tokens = M3ETypeStyleTokens.fromTextStyle(style).copyWith(
letterSpacing: 0.25,
);
final custom = tokens.toTextStyle(fontFamily: 'Roboto Flex');
M3EMaterialApp additionally supports autoTheming (platform brightness) and
dynamicColoring (OS seed color on supported platforms — Material You primary
on Android 12+, accent color on desktop — with schemes generated via
ColorScheme.fromSeed). Pass fontFamily / fontFamilyFallback /
fontVariations, typeScaleMode, typeface, or variableFont to apply
type-scale knobs at the shell. Use buildM3EThemeDefaults()
to assemble a full M3EThemeData
from core tokens, or M3EDynamicColorHost
when you need device dynamic color outside M3EMaterialApp:
M3EMaterialApp(
data: M3EThemeData.light(seedColor: seed),
fontFamily: 'Roboto Flex',
typeScaleMode: M3ETypeScaleMode.variable,
variableFont: const M3EVariableFontConfig(),
home: const HomePage(),
);
M3ETypeVariations is an enum of Roboto Flex axis presets (.variations):
regular, graded (alias for the weight+grade preset formerly named emphasized),
condensed, extra condensed, wide, extra wide, and round. This is not the M3
emphasized type scale — use theme.typography.emphasized for that. Static
and mono fonts ignore axes they do not define. The shell projects the baseline
type scale onto ThemeData.textTheme and DefaultTextStyle.
Components
Every component is a widget you construct directly. Snippets below use a single
import. Stateful controls show // in State where a setState wrapper is
needed.
Actions
See also:
example/lib/pages/playground/do/(Do tab)
M3EButton
Text button aligned with the Material 3 Expressive spec: five sizes, press
shape morph, and InkSparkle. Selection uses isSelected, selectedIcon,
and selectedLabel.
M3EButton(
style: M3EButtonStyle.elevated,
onPressed: () {},
child: const Text('Elevated'),
);
M3EButton(
decoration: M3EButtonDecoration(
backgroundGradient: WidgetStateProperty.all(
const LinearGradient(colors: [Color(0xFF6750A4), Color(0xFF9A82DB)]),
),
foregroundGradient: WidgetStateProperty.all(
const LinearGradient(colors: [Color(0xFFFFFFFF), Color(0xFFEADDFF)]),
),
outlineGradient: WidgetStateProperty.all(
const LinearGradient(colors: [Color(0xFF6750A4), Color(0xFF9A82DB)]),
),
side: WidgetStateProperty.all(const BorderSide(width: 1)),
),
onPressed: () {},
child: const Text('Gradient'),
);
Keyboard: Tab, then Space or Enter.
M3EIconButton
Icon-only button aligned with the Material 3 Expressive spec. Default variant
is filled. Toggle with isSelected and selectedIcon.
M3EIconButton(
icon: const Icon(M3EIcons.edit),
variant: M3EIconButtonVariant.filled,
onPressed: () {},
);
// in State
M3EIconButton(
icon: const Icon(M3EIcons.add),
selectedIcon: const Icon(M3EIcons.check),
isSelected: isFavorite,
onPressed: () => setState(() => isFavorite = !isFavorite),
);
Keyboard: Tab, then Space or Enter.
M3EFab
Floating action button aligned with the Material 3 Expressive spec. Sizes:
small 40, regular 56, medium 80 (default), large 96. M3EFabController
handles scroll, appear, and container transform.
final fabController = M3EFabController();
M3EFabScrollVisibility(
controller: fabController,
child: Scaffold(
body: ListView(...),
floatingActionButton: M3EFab(
controller: fabController,
appear: true,
icon: const Icon(M3EIcons.add),
size: M3EFabSize.medium,
color: M3EFabColor.primaryFilled,
elevation: 3,
hoverElevation: 4,
openBuilder: (context) => const ComposePage(),
onPressed: () {},
),
),
);
M3EFab(
icon: const Icon(M3EIcons.add),
size: M3EFabSize.large,
color: M3EFabColor.tertiary,
elevation: 3,
hoverElevation: 4,
onPressed: () {},
);
Keyboard: Tab, then Space or Enter.
M3EExtendedFab
Extended FAB aligned with the Material 3 Expressive spec. Label is required.
Sizes are small 56 (default), medium 80, and large 96.
M3EExtendedFabController handles scroll, appear, and container transform.
final fabController = M3EExtendedFabController();
M3EExtendedFabScrollVisibility(
controller: fabController,
child: Scaffold(
body: ListView(...),
floatingActionButton: M3EExtendedFab(
controller: fabController,
appear: true,
label: 'Compose',
icon: const Icon(M3EIcons.edit),
size: M3EExtendedFabSize.small,
color: M3EFabColor.primary,
openBuilder: (context) => const ComposePage(),
onPressed: () {},
),
),
);
M3EExtendedFab(
label: 'Compose',
icon: const Icon(M3EIcons.edit),
size: M3EExtendedFabSize.medium,
color: M3EFabColor.primaryFilled,
onPressed: () {},
);
Keyboard: Tab, then Space or Enter.
M3EFabMenu
Speed-dial menu of 2–6 items, aligned with the Material 3 Expressive spec. The trigger becomes a 56dp close button. Back closes the menu before the route.
final menuController = M3EFabMenuController();
M3EFabMenu(
controller: menuController,
position: M3EFabMenuPosition.right,
size: M3EFabSize.medium,
color: M3EFabColor.primary,
expandIcon: const Icon(M3EIcons.add),
collapseIcon: const Icon(M3EIcons.close),
items: [
M3EFabMenuItem(
icon: const Icon(M3EIcons.edit),
label: 'Note',
onPressed: () {},
openBuilder: (context) => const NotePage(),
),
M3EFabMenuItem(
icon: const Icon(M3EIcons.schedule),
label: 'Reminder',
onPressed: () {},
),
],
);
Keyboard: Tab walks items. Escape closes.
M3EButtonGroup
Connected or standard groups aligned with the Material 3 Expressive spec.
Actions are M3EButton. Density changes height, not the gap.
// in State — single-select
M3EButtonGroup(
selectedIndex: groupIndex,
onSelectedIndexChanged: (i) => setState(() => groupIndex = i),
selectionRequired: true,
actions: const [
M3EButtonGroupAction(icon: Icon(M3EIcons.arrow_back), minWidth: 40),
M3EButtonGroupAction(icon: Icon(M3EIcons.add), minWidth: 40),
M3EButtonGroupAction(icon: Icon(M3EIcons.arrow_forward), minWidth: 40),
],
);
// multi-select
M3EButtonGroup(
multiSelect: true,
selectedIndices: selected,
onSelectedIndicesChanged: (s) => setState(() => selected = s),
type: M3EButtonGroupType.connected,
actions: const [
M3EButtonGroupAction(label: Text('Mon')),
M3EButtonGroupAction(label: Text('Tue')),
M3EButtonGroupAction(label: Text('Wed')),
],
);
Keyboard: Tab, then Space or Enter. Arrows are not captured.
Button selection
Set M3EButton.isSelected to enable caller-controlled selection with
round-to-square (or square-to-round) shape morphing. selectedIcon and
selectedLabel replace their unselected counterparts. Selection is not
available for M3EButtonStyle.text. M3EButtonGroup accepts a group-level
M3EButtonDecoration and per-action M3EButtonGroupAction.decoration.
// in State
M3EButton.filled(
icon: const Icon(M3EIcons.favorite_border),
selectedIcon: const Icon(M3EIcons.favorite),
isSelected: isFavorite,
onPressed: () => setState(() => isFavorite = !isFavorite),
);
Keyboard: Tab, then Space or Enter.
M3ESegmentedButton
Outlined single- or multi-select control, aligned with the Material 3 Expressive spec. Two to five segments. Density lowers the height; the target stays at least 48.
// in State — single select
M3ESegmentedButton<String>(
segments: const [
M3ESegment(value: 'list', label: 'List'),
M3ESegment(value: 'grid', label: 'Grid'),
],
selected: viewMode,
onSelectionChanged: (v) => setState(() => viewMode = v),
);
// multi select
M3ESegmentedButton<String>(
multiSelect: true,
density: M3ESegmentedButtonDensity.comfortable,
segments: const [
M3ESegment(value: 'new', label: 'New'),
M3ESegment(value: 'sale', label: 'Sale'),
],
selected: filters,
onSelectionChanged: (v) => setState(() => filters = v),
);
Keyboard: Tab, then Space or Enter.
M3ESplitButton
Primary action plus a menu, aligned with the Material 3 Expressive spec. The gap between the two segments is 2. Back closes the popup before the route.
M3ESplitButton<String>(
label: 'Save',
leadingIcon: M3EIcons.check,
onPressed: () {},
onSelected: (value) {},
items: const [
M3ESplitButtonItem(value: 'draft', child: Text('Save as draft')),
M3ESplitButtonItem(value: 'copy', child: Text('Save a copy')),
],
);
M3ESplitButton<String>(
label: 'Share',
items: null,
onSelected: (value) {},
m3eMenuBuilder: (context) => [
M3EMenuSelectable(label: 'Copy link', value: 'link'),
const M3EMenuDivider(),
M3EMenuSelectable(label: 'Email', value: 'email'),
],
);
Keyboard: Tab, then Space or Enter. Escape closes the menu.
Selection
See also:
example/lib/pages/playground/pick/(Pick tab)
M3ECheckbox
Checkbox aligned with the Material 3 Expressive spec: 18dp box, 2dp corners,
40dp state layer, 48dp target. Optional label. checkIconPadding defaults to
none.
// in State
M3ECheckbox(
value: checked,
onChanged: (v) => setState(() => checked = v),
);
M3ECheckbox(
value: tristateValue,
tristate: true,
label: const Text('Remember me'),
onChanged: (v) => setState(() => tristateValue = v),
);
Keyboard: Tab, then Space or Enter.
M3ERadio
Radio aligned with the Material 3 Expressive spec: 20dp icon, 40dp state
layer, 48dp target. Put options in M3ERadioGroup for arrow-key selection.
// in State
M3ERadioGroup<String>(
groupValue: plan,
groupLabel: 'Plan',
onChanged: (v) => setState(() => plan = v),
child: M3ERadio<String>(
value: 'pro',
groupValue: plan,
label: const Text('Pro'),
onChanged: (v) => setState(() => plan = v),
),
);
Keyboard: Tab or Shift+Tab enters the selected radio. Arrows move, select, and wrap.
M3ESwitch
Switch aligned with the Material 3 Expressive spec. Track is 52×32. The handle is 16 off, 24 on or with an icon, and 28 pressed. Drag past the midpoint toggles.
// in State
M3ESwitch(
value: wifiEnabled,
selectedIcon: const Icon(M3EIcons.check),
onChanged: (v) => setState(() => wifiEnabled = v),
);
M3ESwitch(
value: bluetoothEnabled,
stateLayerSize: 56,
onChanged: (v) => setState(() => bluetoothEnabled = v),
);
Keyboard: Tab, then Space or Enter.
M3EChip
Chips aligned with the Material 3 Expressive spec: height 32, radius 8.
M3EChipGroup moves focus with the arrow keys.
M3EChip(
label: 'Assist',
leading: const Icon(M3EIcons.edit),
onPressed: () {},
);
// in State — filter chip
M3EChip(
label: 'Flutter',
type: M3EChipType.filter,
selected: chips.contains('flutter'),
onPressed: () => toggleChip('flutter'),
);
M3EChipGroup(
child: Wrap(
spacing: 8,
children: [
M3EChip(
label: 'Dart',
type: M3EChipType.input,
avatar: const Icon(M3EIcons.person),
onPressed: () {},
onDeleted: () {},
),
],
),
);
Keyboard: arrows move focus. Backspace or Delete removes a focused input chip.
M3EDropdownMenu
Dropdown for one value, many values, search, or async items. Back closes the panel before the route.
// Single select
M3EDropdownMenu<String>(
singleSelect: true,
items: const [
M3EDropdownItem(label: 'Flutter', value: 'flutter'),
M3EDropdownItem(label: 'Dart', value: 'dart'),
],
fieldStyle: const M3EDropdownFieldStyle(hintText: 'Choose a framework'),
onSelectionChanged: (items) {},
);
// Multi select with search (and optional selection cap)
M3EDropdownMenu<String>(
searchEnabled: true,
limit: 2,
items: const [
M3EDropdownItem(label: 'Layout', value: 'layout'),
M3EDropdownItem(label: 'Theming', value: 'theming'),
],
fieldStyle: const M3EDropdownFieldStyle(hintText: 'Select skills'),
onSelectionChanged: (items) {},
);
// Async items
M3EDropdownMenu<String>.future(
singleSelect: true,
future: () async => [
const M3EDropdownItem(label: 'Ghana', value: 'gh'),
const M3EDropdownItem(label: 'Kenya', value: 'ke'),
],
fieldStyle: const M3EDropdownFieldStyle(hintText: 'Load countries'),
onSelectionChanged: (items) {},
);
Keyboard: Enter or Space opens. Escape closes. Arrows move inside the panel.
M3ESlider
Slider for a value or a range, including centered, wavy, and vertical,
aligned with the Material 3 Expressive spec: sizes xs–xl scale the track
(16–96) and handle (44–108) together. Active track and handle are
primary; inactive track is secondary container. Stops use
divisions.
// in State
M3ESlider(
value: volume,
onChanged: (v) => setState(() => volume = v),
);
// Spec size — track and handle scale together (defaults to xs)
M3ESlider(
value: level,
size: M3ESliderSize.l,
semanticLabel: 'Volume',
onChanged: (v) => setState(() => level = v),
);
M3ESlider(
value: brightness,
max: 5,
divisions: 5,
onChanged: (v) => setState(() => brightness = v),
);
// Wavy active value (inactive track stays flat)
M3ESlider.wavy(
value: progress,
onChanged: (v) => setState(() => progress = v),
);
M3ESlider.centered(
value: balance,
min: -100,
max: 100,
onChanged: (v) => setState(() => balance = v),
);
// Custom track / thumb / end dots (size & edge padding)
M3ESlider(
value: level,
max: 4,
divisions: 4,
trackThickness: 30,
cornerRadius: 8,
thumbLength: 50,
dotSize: 12,
dotSpacing: 10,
onChanged: (v) => setState(() => level = v),
dotBuilder: ({
required context,
required color,
required size,
required active,
}) {
// e.g. paint M3EMaterialNewShapes.cookie4Sided / softBurst
return ColoredBox(color: color);
},
);
M3ERangeSlider(
values: range,
onChanged: (v) => setState(() => range = v),
);
M3ERangeSlider.wavy(
values: range,
onChanged: (v) => setState(() => range = v),
);
SizedBox(
height: 160,
width: 48,
child: M3ESlider.vertical(
value: level,
onChanged: (v) => setState(() => level = v),
),
);
Keyboard: arrows step. Page Up and Page Down jump. Home and End go to the ends.
M3EDatePicker
Dialog and inline calendar for one date or a range. Month paging uses the arrow keys.
// Inline calendar
M3ECalendarDatePicker(
initialDate: date,
firstDate: DateTime(2020),
lastDate: DateTime(2030),
onDateChanged: (v) => setState(() => date = v),
);
// Dialog
final picked = await M3EDatePicker.show(
context,
initialDate: date,
firstDate: DateTime(2020),
lastDate: DateTime(2030),
);
// Range dialog
final range = await M3EDatePicker.showRange(
context,
firstDate: DateTime(2020),
lastDate: DateTime(2030),
);
Keyboard: arrows change month. Enter moves focus.
M3ETimePicker
Dialog and dial for a time of day.
// Dialog
final M3ETime? picked = await M3ETimePicker.show(
context,
initialTime: time,
);
// Inline dial
M3EDialTimePicker(
value: time,
onChanged: (v) => setState(() => time = v),
);
Keyboard: Enter moves focus.
Containment
See also:
example/lib/pages/playground/view/(View tab)
M3ECard
Elevated, filled, and outlined surface for content and actions, aligned with
the Material 3 Expressive spec: content padding 16 on every side, focus
ring secondary at 3dp. Optional media, headline, supporting text,
actions, overflow menu, dividers, one swipe action, and a full-screen
openBuilder container transform.
M3ECard(child: const Text('Elevated'));
M3ECard(
variant: M3ECardVariant.filled,
child: const Text('Filled'),
);
M3ECard(
variant: M3ECardVariant.outlined,
onPressed: () {},
child: const Text('Outlined (tap)'),
);
// Structured slots — vertical stacks media above the text
M3ECard(
vertical: true,
media: Image.network(imageUrl, fit: BoxFit.cover),
headline: const Text('Weekend trip'),
supportingText: const Text('12 photos · 3 people'),
dividerAfterMedia: true,
actions: M3EButton.text(onPressed: () {}, child: const Text('Share')),
overflow: M3EIconButton(
icon: const Icon(M3EIcons.more_vert),
onPressed: () {},
),
);
// Swipe to reveal a trailing action, or flick to dismiss
M3ECard(
swipeMode: M3ECardSwipeMode.both,
trailingSwipeAction: const Icon(M3EIcons.delete),
onSwipe: () {},
child: const Text('Swipe me'),
);
// Full-screen container transform
M3ECard(
openBuilder: (context) => const Scaffold(body: Center(child: Text('Detail'))),
child: const Text('Tap to open'),
);
// A group of cards sharing a gap, elevation, and layout
M3ECardGroup(
layout: M3ECardGroupLayout.staggered,
onReorder: (oldIndex, newIndex) {},
children: const [
M3ECard(child: Text('One')),
M3ECard(child: Text('Two')),
],
);
M3ECarousel
Multi-browse, uncontained, uncontained multi-aspect, hero, and full-screen
layouts — horizontal by default, or vertical via axis. Items are
M3ECarouselItem values; a null onTap on an item disables it. Use
onChange for leading/focal index updates (e.g. hide labels on smaller
items).
M3ECarousel(
type: M3ECarouselType.hero,
heroAlignment: M3ECarouselHeroAlignment.center,
onTap: (index) {},
onChange: (details) {
// details.focalIndex / details.leadingIndex / details.isFocal(i)
},
children: List.generate(
10,
(i) => M3ECarouselItem(
image: Image.network(imageUrls[i], fit: BoxFit.cover),
title: Text('Item $i'),
subtitle: const Text('Subtitle'),
onTap: () {},
),
),
);
// Step, jump, or open the full list with a controller
final carouselController = M3ECarouselController();
M3ECarousel(
controller: carouselController,
type: M3ECarouselType.uncontained,
showAll: true,
children: const [...],
);
// carouselController.next() / .previous() / .animateToItem(i) / .jumpToItem(i)
M3EListItem
Standard list row with headline, supporting text, and slots. Optional
variant / border control the standalone card outline.
M3EListItem(
headline: 'Wireless charging',
supportingText: 'On · Fast charge enabled',
leading: const Icon(M3EIcons.schedule),
trailing: const Icon(M3EIcons.chevron_right),
variant: M3ECardVariant.outlined,
onTap: () {},
);
M3EList
One list. List-level fields set the variant, selection, and reorder. Each
M3EListItem can opt into swipe, expanded, and transform. A sub-list
expansion is its own nested M3EList, which inherits the parent corner join,
fill, and variant. Use .scrollable for a lazy list and .sliver inside a
CustomScrollView (slivers keep selection and do not reorder). Resting
corners (M3EListStyle.segmented, the default, vs .standard) are set on
M3EListTheme, like other component styling.
M3EList(
variant: M3ECardVariant.outlined,
itemCount: 3,
onTap: (index) {},
itemBuilder: (context, index) => M3EListItem(
headline: 'Inbox',
leading: const Icon(M3EIcons.schedule),
swipe: M3EListItemSwipe(
onDismiss: (direction) async => true,
trailing: const [
M3EListSwipeAction(
icon: Icon(M3EIcons.delete),
isPrimary: true,
),
],
),
expanded: M3EExpandableExpanded.list(
M3EList(
embedded: true,
itemCount: 2,
itemBuilder: (context, i) => M3EListItem(headline: 'Child $i'),
),
),
),
);
// Scrollable / lazy
M3EList.scrollable(
itemCount: 20,
shrinkWrap: true,
itemBuilder: (context, index) => M3EListItem(
headline: 'Item $index',
),
);
M3ESelection
Multi-select host with an optional app bar and any list as the body. Back clears the selection before leaving the page.
final selection = M3ESelectionController();
PopScope(
canPop: !selection.isSelectionMode,
onPopInvokedWithResult: (didPop, _) {
if (!didPop) selection.clear();
},
child: M3ESelection(
controller: selection,
itemCount: items.length,
selectedColor: const Color(0xFFC8E6C9),
appBar: M3ESelectionAppBar(
idle: M3EAppBar.search(
searchController: searchController,
suggestionsBuilder: (_, __) => const [],
barHintText: 'Search items',
),
actions: [
M3EIconButton(icon: Icon(M3EIcons.archive), onPressed: () {}),
M3EIconButton(icon: Icon(M3EIcons.delete), onPressed: () {}),
],
),
body: M3EList.scrollable(
itemCount: items.length,
listPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
selection: true,
selectionController: selection,
selectionState: const M3EListSelectionState(
selectedIcon: Icon(M3EIcons.check),
),
itemBuilder: (context, i) => M3EListItem(headline: items[i]),
),
),
);
Advanced: wire M3ESelectionAppBar + a shared controller yourself (omit
M3ESelection), or set M3EListItem.swipe for swipe + select.
An explicit colorBuilder still wins over the selection highlight.
M3EDivider
Divider aligned with the Material 3 Expressive spec: a 1dp outline-variant
line. Full width unless inset or outerMargin is set.
const M3EDivider();
const M3EDivider(inset: M3EDividerInset.inset);
Row(
children: [
const Text('Left'),
const SizedBox(width: 12),
const M3EDivider(axis: M3EDividerAxis.vertical),
const SizedBox(width: 12),
const Text('Right'),
],
);
M3EDialog
Modal dialog — use the static .show helper. Optional topDivider /
bottomDivider draw full-bleed lines between header, content, and actions
(padding lives on those sections so dividers can reach the edges).
M3EDialog.show<void>(
context,
dialog: M3EDialog(
title: 'Reset settings?',
content: const Text('This restores default values.'),
topDivider: true,
bottomDivider: true,
actions: [
M3EButton(
style: M3EButtonStyle.text,
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
M3EButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Reset'),
),
],
),
);
// Selection list (single or multi); confirm disabled until a choice is made
final List<String>? picked = await M3EDialog.showSelectionScreen(
context,
title: 'Choose a plan',
options: const <String>['Standard', 'Pro', 'Team'],
multiSelect: false,
);
// Full-screen variant
M3EDialog.showFullScreen<void>(
context,
title: 'New event',
body: const Padding(
padding: EdgeInsets.all(24),
child: Text('Full-screen dialog body.'),
),
);
M3EBottomSheet
Modal bottom sheet — use .show.
M3EBottomSheet.show<void>(
context,
builder: (context) => const Padding(
padding: EdgeInsets.all(24),
child: Text('A modal bottom sheet with a drag handle.'),
),
);
M3ESideSheet
Side sheet panel — use .show.
M3ESideSheet.show<void>(
context,
title: 'Filters',
body: const Padding(
padding: EdgeInsets.all(24),
child: Text('Side sheet content.'),
),
);
Navigation
See also:
example/lib/pages/playground/nav/(Nav tab)
M3EAppBar
Top, search, sliver, and bottom app bar variants, aligned with the
Material 3 Expressive spec: small content band is 64, flexible medium
112 and large 120 (taller with a subtitle). Docked top/bottom bars
apply single-edge safeArea padding from MediaQuery.viewPadding by
default (opt out with safeArea: false).
M3EAppBar.top(
titleText: 'Inbox',
leading: const Icon(M3EIcons.menu),
actions: const [Icon(M3EIcons.search)],
);
// Anchored search title — tap opens fullscreen (or docked) search.
// Idle pill content (leading + hint + trailing) defaults to Alignment.center.
M3EAppBar.search(
searchController: searchController,
barHintText: 'Search mail',
leading: const Icon(M3EIcons.menu),
actions: const [Icon(M3EIcons.tune)],
suggestionsBuilder: (context, controller) sync* {
yield const ListTile(title: Text('Suggestion'));
},
);
// Sliver (inside CustomScrollView)
M3EAppBar.sliver(
titleText: 'Sliver • medium',
actions: const [Icon(M3EIcons.search)],
);
// Bottom app bar with FAB slot
M3EAppBar.bottom(
actions: const [Icon(M3EIcons.menu), Icon(M3EIcons.search)],
floatingActionButton: M3EFab(
icon: const Icon(M3EIcons.add),
size: M3EFabSize.small,
onPressed: () {},
),
);
// Sliver with a controller and an actions-only hide mode — the action row
// stays on its own fill while the title/image slide away on scroll.
final appBarController = M3EAppBarController();
M3EAppBar.sliver(
controller: appBarController,
hideMode: M3EAppBarHideMode.actions,
titleText: 'Inbox',
actions: const [Icon(M3EIcons.search)],
);
// appBarController.expand() / .collapse() / .show() / .hide() / .followScroll()
Collapse and hide on scroll with M3EAppBar.top / .search. A
Scaffold starts its body where the app bar ends, so a bar that shrinks
drags the body up while the list inside it is also scrolling: the content
then moves faster than the bar and slides under its edge. No bar can fix
that from inside the appBar slot. Set extendBodyBehindAppBar: true and
the page runs behind the bar instead: the bar keeps a fixed slot, only its
surface moves, and it travels exactly with the content. ListView,
GridView, and CustomScrollView pick up the bar height as top padding
from the Scaffold on their own; add it yourself for anything else
(MediaQuery.paddingOf(context).top).
Scaffold(
extendBodyBehindAppBar: true, // bar moves 1:1 with the content
appBar: M3EAppBar.top(
titleText: 'Inbox',
variant: M3EAppBarVariant.mediumFlexible,
hideMode: M3EAppBarHideMode.entire,
),
body: ListView(children: messages),
);
Without the flag, collapse still works but the content moves ahead of the
bar, and hiding falls back to a timed slide. M3EAppBar.sliver needs no
flag: it is part of the scroll content, so it always moves with it.
M3ETabs
Primary and secondary tab bars, aligned with the Material 3 Expressive spec: primary label-only and secondary bars are 48; primary icon plus label is 64.
// in State
M3ETabs(
selectedIndex: tabIndex,
onTabSelected: (i) => setState(() => tabIndex = i),
tabs: const [
M3ETab(label: 'Overview'),
M3ETab(label: 'Specs'),
M3ETab(label: 'Reviews'),
],
);
M3ETabs(
variant: M3ETabsVariant.secondary,
selectedIndex: tabIndex,
onTabSelected: (i) => setState(() => tabIndex = i),
tabs: const [
M3ETab(label: 'Photos', icon: Icon(M3EIcons.calendar_today)),
M3ETab(label: 'Albums', icon: Icon(M3EIcons.menu)),
],
);
// Swipeable body kept in sync with the bar, plus a selection controller
final tabsController = M3ETabsController();
M3ETabs(
controller: tabsController,
selectedIndex: tabIndex,
onTabSelected: (i) => setState(() => tabIndex = i),
tabs: const [M3ETab(label: 'Overview'), M3ETab(label: 'Specs')],
);
M3ETabsView(
selectedIndex: tabIndex,
onTabSelected: (i) => setState(() => tabIndex = i),
children: const [Text('Overview'), Text('Specs')],
);
// Sliver — scrolls away and returns on an upward scroll
M3ETabs.sliver(
selectedIndex: tabIndex,
onTabSelected: (i) => setState(() => tabIndex = i),
tabs: const [M3ETab(label: 'Overview'), M3ETab(label: 'Specs')],
);
M3ENavigationBar
Bottom navigation aligned with the Material 3 Expressive spec. The selected pill scales in place. Wide layout is a row of icon and label chips.
// in State
M3ENavigationBar(
destinations: const [
M3ENavigationBarDestination(icon: Icon(M3EIcons.menu), label: 'Home'),
M3ENavigationBarDestination(
icon: Icon(M3EIcons.search),
label: 'Search',
badgeDot: true,
),
],
selectedIndex: barIndex,
onDestinationSelected: (i) => setState(() => barIndex = i),
);
// Force wide layout (autoLayout off) with end-aligned chips
M3ENavigationBar(
autoLayout: false,
layout: M3ENavBarLayout.wide,
alignment: M3ENavBarAlignment.end,
wideDestinationWidth: 128,
iconBehavior: M3ENavBarIconBehavior.alwaysShow,
labelBehavior: M3ENavBarLabelBehavior.alwaysShow,
destinations: const [
M3ENavigationBarDestination(icon: Icon(M3EIcons.home), label: 'Home'),
M3ENavigationBarDestination(label: 'Browse'), // label-only
M3ENavigationBarDestination(icon: Icon(M3EIcons.radio)), // icon-only
],
selectedIndex: barIndex,
onDestinationSelected: (i) => setState(() => barIndex = i),
);
// Custom autoLayout breakpoint + chip width
M3ENavigationBar(
autoLayout: true,
wideBreakpoint: 720,
wideDestinationWidth: 140,
destinations: const [
M3ENavigationBarDestination(icon: Icon(M3EIcons.home), label: 'Home'),
M3ENavigationBarDestination(icon: Icon(M3EIcons.search), label: 'Search'),
],
selectedIndex: barIndex,
onDestinationSelected: (i) => setState(() => barIndex = i),
);
// Hide on a downward scroll, with a controller for manual show/hide/select
final navBarController = M3ENavigationBarController();
M3ENavigationBar(
controller: navBarController,
hideOnScroll: true,
scrollController: listScrollController,
destinations: const [...],
selectedIndex: barIndex,
onDestinationSelected: (i) => setState(() => barIndex = i),
);
// navBarController.show() / .hide() / .select(i)
M3ENavigationRail
Vertical navigation aligned with the Material 3 Expressive spec: collapsed
width 96 (narrow 80), expanded 220–360. The selected pill scales
in place. Collapsed pills match the navigation bar. A modal rail dismisses
on the scrim, Escape, or system back. Horizontal body scroll raises the
rail's container automatically (scrollUnder, default on).
// in State
M3ENavigationRail(
sections: const [
M3ENavigationRailSection(
destinations: [
M3ENavigationRailDestination(
icon: Icon(M3EIcons.menu),
label: 'Home',
),
M3ENavigationRailDestination(
icon: Icon(M3EIcons.search),
label: 'Search',
),
],
),
],
selectedIndex: railIndex,
onDestinationSelected: (i) => setState(() => railIndex = i),
expandTooltip: 'Expand',
collapseTooltip: 'Collapse',
fab: M3ENavigationRailFabSlot(
icon: const Icon(M3EIcons.add),
label: 'Compose',
elevation: 3,
hoverElevation: 4,
onPressed: () {},
),
);
// Leading control, a divider on the content edge, and a controller
final railController = M3ENavigationRailController();
M3ENavigationRail(
controller: railController,
leading: M3EIconButton(
icon: const Icon(M3EIcons.menu),
onPressed: railController.toggle,
),
showDivider: true,
alignment: M3ENavigationRailAlignment.center,
sections: const [...],
selectedIndex: railIndex,
onDestinationSelected: (i) => setState(() => railIndex = i),
);
// railController.expand() / .collapse() / .select(i) / .show() / .hide()
M3ENavigationDrawer
Standard (default) or modal drawer aligned with the Material 3 Expressive
spec: width 360, end corners 16. The selected pill scales in place.
A modal drawer opens from a button and dismisses on a destination, the
scrim, a drag toward the start edge, or system back. A dismissible standard
drawer closes only from its controller.
// in State
M3ENavigationDrawer(
headline: 'Mail',
destinations: const [
M3ENavigationDestination(icon: Icon(M3EIcons.menu), label: 'Home'),
M3ENavigationDestination(
icon: Icon(M3EIcons.search),
label: 'Search',
showBadge: true,
),
],
sections: const [
M3ENavigationDrawerSection(
header: 'Labels',
destinations: [
M3ENavigationDestination(icon: Icon(M3EIcons.favorite), label: 'Starred'),
],
),
],
selectedIndex: drawerIndex,
onDestinationSelected: (i) => setState(() => drawerIndex = i),
);
// Modal drawer, opened from a button, with a controller
final drawerController = M3ENavigationDrawerController();
M3EIconButton(
icon: const Icon(M3EIcons.menu),
onPressed: drawerController.open,
);
M3ENavigationDrawer(
type: M3ENavigationDrawerType.modal,
controller: drawerController,
onDismissed: () {},
destinations: const [...],
selectedIndex: drawerIndex,
onDestinationSelected: (i) => setState(() => drawerIndex = i),
);
M3EToolbar
Floating or docked toolbar, both 64 tall, aligned with the Material 3
Expressive spec. Floating placement uses alignment. screenOffset
(default 16) keeps the pill off the screen edge. Docked ignores both and
uses contentAlignment to place actions at 600dp and wider.
// Floating (default) — pill, wrap-content
M3EToolbar(
actions: <M3EToolbarItem>[
M3EToolbarAction(icon: M3EIcons.edit, onPressed: () {}),
M3EToolbarAction(icon: M3EIcons.share, onPressed: () {}),
],
);
// Floating + expand trigger + adjacent FAB
M3EToolbar(
expanded: true,
onExpandedChanged: (open) {},
actions: <M3EToolbarItem>[
M3EToolbarAction(
icon: M3EIcons.menu,
isExpandTrigger: true,
onPressed: () {},
),
M3EToolbarAction(icon: M3EIcons.edit, onPressed: () {}),
M3EToolbarAction(icon: M3EIcons.share, onPressed: () {}),
],
fabIcon: const Icon(M3EIcons.add),
fabExpandIcon: const Icon(M3EIcons.add),
fabCollapseIcon: const Icon(M3EIcons.close),
onFabPressed: () {},
);
// Small FAB — no pill expand/collapse (only onFabPressed)
M3EToolbar(
fabExpandsToolbar: false,
onFabPressed: () {},
fabExpandIcon: const Icon(M3EIcons.add),
actions: <M3EToolbarItem>[...],
);
// Action selection (internal active index when onActiveIndexChanged is set)
M3EToolbar(
onActiveIndexChanged: (i) {},
actions: <M3EToolbarItem>[
M3EToolbarAction(
icon: M3EIcons.edit,
label: 'Edit',
onPressed: () {},
),
M3EToolbarAction(icon: M3EIcons.share, onPressed: () {}),
],
);
// Labeled selection — fixed pill width (action labels still spring)
M3EToolbar(
pillActiveSpring: false,
onActiveIndexChanged: (i) {},
actions: <M3EToolbarItem>[...],
);
// Scroll-exit / manual visibility
final visibility = M3EToolbarVisibilityController();
M3EToolbarScrollWrapper(
behavior: M3EToolbarScrollBehavior.exitAlways(controller: visibility),
child: ListView(...),
);
M3EToolbar(
visibilityController: visibility,
actions: <M3EToolbarItem>[...],
);
// Mixed icon actions + custom widgets (widgets stay inline; height-capped)
M3EToolbar(
actions: <M3EToolbarItem>[
M3EToolbarAction(icon: M3EIcons.edit, onPressed: () {}),
M3EToolbarWidget(
child: M3ESplitButton<String>(
size: M3EButtonSize.sm,
label: 'Sort',
items: const [
M3ESplitButtonItem(value: 'name', child: 'Name'),
M3ESplitButtonItem(value: 'date', child: 'Date'),
],
onSelected: (_) {},
),
),
M3EToolbarAction(icon: M3EIcons.share, onPressed: () {}),
],
);
// Vertical floating
M3EToolbar(
axis: Axis.vertical,
colorStyle: M3EToolbarColorStyle.vibrant,
actions: <M3EToolbarItem>[...],
);
// Docked — full width; safeArea pads only the dock edge
M3EToolbar.docked(
dockEdge: M3EToolbarDockEdge.bottom,
safeArea: true,
titleText: 'Inbox',
// At 600dp and wider: even (default), centered, or edges.
contentAlignment: M3EToolbarContentAlignment.even,
actions: <M3EToolbarItem>[
M3EToolbarAction(icon: M3EIcons.search, onPressed: () {}),
M3EToolbarAction(
icon: M3EIcons.delete,
label: 'Delete',
isDestructive: true,
onPressed: () {},
),
],
);
M3EMenu
Menu aligned with the Material 3 Expressive spec. Variants are vertical and baseline. Opening focuses the first enabled item. Multi-select stays open. Back closes a submenu, then the menu, before the route.
M3EMenu(
anchorBuilder: (context, open) => M3EButton.icon(
style: M3EButtonStyle.outlined,
icon: const Icon(M3EIcons.arrow_drop_down),
label: const Text('Open menu'),
onPressed: open,
),
children: [
M3EMenuGroup.entries(
entries: [
M3EMenuEntry(
label: 'Edit',
leading: const Icon(M3EIcons.edit),
onPressed: () {},
),
const M3EMenuEntry(label: 'Disabled', enabled: false),
],
),
M3EMenuGroup.entries(
label: 'More',
entries: [
M3EMenuEntry(
label: 'Copy',
trailingText: '⌘C',
onPressed: () {},
),
],
),
],
);
Keyboard: Up and Down move. Left and Right open or close a submenu. Letters jump. Escape closes. Enter or Space activates.
Feedback
See also:
example/lib/pages/playground/find/(Find tab)
M3EBadge
Badge aligned with the Material 3 Expressive spec. A 6dp dot or a large label (min 16dp) in error colors. It overlays the child without shifting it.
const M3EBadge(
showDot: true,
child: Icon(M3EIcons.menu, size: 28),
);
const M3EBadge(
count: 8,
alignment: M3EBadgeAlignment.topLeft,
child: Icon(M3EIcons.calendar_today, size: 28),
);
const M3EBadge(
label: 'New',
child: Icon(M3EIcons.mail, size: 28),
);
M3EProgressIndicator
Circular and linear progress aligned with the Material 3 Expressive spec,
including wavy forms. The track is secondary container. Set showTrack: false
to hide it.
// Classic
const M3EProgressIndicator.circular();
M3EProgressIndicator.circular(value: 0.6);
M3EProgressIndicator.circular(
value: 0.6,
trackStrokeWidth: 2,
);
const M3EProgressIndicator.linear();
SizedBox(
width: 200,
child: M3EProgressIndicator.linear(value: 0.6),
);
// Hide track (e.g. inside a button)
M3EProgressIndicator.circular(showTrack: false);
// Expressive wavy (Compose CircularWavy / LinearWavy)
const M3EProgressIndicator.circularWavy();
M3EProgressIndicator.circularWavy(value: 0.6);
SizedBox(
width: 200,
child: M3EProgressIndicator.linearWavy(),
);
SizedBox(
width: 200,
child: M3EProgressIndicator.linearWavy(value: 0.6),
);
M3ELoadingIndicator
Indeterminate loading shape aligned with the Material 3 Expressive spec. Default outer size is 48 and the active shape is 38. There is no elevation.
const M3ELoadingIndicator();
const M3ELoadingIndicator(
variant: M3ELoadingIndicatorVariant.contained,
);
// Ratio-preserving scale (outer 96 → active 76)
const M3ELoadingIndicator(size: 96);
M3ELoadingIndicator(
indicatorSize: 32,
containerWidth: 56,
containerHeight: 56,
containerShape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
indicatorColors: const <Color>[
Color(0xff6750a4),
Color(0xff006a6a),
],
);
// Host-driven rotation (e.g. during pull-to-refresh drag)
M3ELoadingIndicator(
variant: M3ELoadingIndicatorVariant.contained,
rotationTurns: dragTurns,
);
M3ERefreshIndicator
Pull-to-refresh wrapper for scrollables. Default and .contained kinds always
build a contained M3ELoadingIndicator; optional elevation is applied on
the refresh host shell (including Flutter web). Reveal starts after
2 × indicatorPadding; arm / refresh only when fully revealed. List pad is
capped by contentDragOffset (defaults to indicator height +
2 × indicatorPadding). Use M3ERefreshIndicatorController (or a
GlobalKey<M3ERefreshIndicatorState>) for programmatic show().
final controller = M3ERefreshIndicatorController();
M3ERefreshIndicator(
controller: controller,
onRefresh: () async {
await Future<void>.delayed(const Duration(seconds: 2));
},
child: ListView.builder(
itemCount: 12,
itemBuilder: (context, index) => Text('Item ${index + 1}'),
),
);
// Contained shell + optional elevation / pad overrides
M3ERefreshIndicator.contained(
controller: controller,
elevation: 3,
indicatorPadding: 8,
contentDragOffset: 72,
onRefresh: () async {},
child: listView,
);
// Manual trigger
await controller.show();
M3ETooltip
Plain or rich tooltip aligned with the Material 3 Expressive spec. Plain sits
above the target. Rich can stay open with persistent.
M3ETooltip(
message: 'Compose a new message',
child: M3EIconButton(
icon: const Icon(M3EIcons.edit),
onPressed: () {},
),
);
M3ETooltip(
persistent: true,
richTitle: 'Compose',
richMessage: 'Start a new draft with expressive defaults.',
actions: <Widget>[
M3EButton.text(onPressed: () {}, child: Text('Got it')),
],
child: M3EIconButton(
icon: const Icon(M3EIcons.edit),
onPressed: () {},
),
);
M3ESnackbar
Brief message aligned with the Material 3 Expressive spec. A bar with an action or close button stays until dismissed. Escape dismisses it when focused.
M3ESnackbar.show(
context,
message: 'Draft saved',
actionLabel: 'Undo',
onAction: () {},
showCloseButton: true,
);
Keyboard: Escape dismisses when focused.
M3ETextField
Filled or outlined field with a floating label. Height grows with maxLines.
M3ETextField(
controller: nameController,
label: 'Full name',
supportingText: 'As it appears on your ID',
leading: const Icon(M3EIcons.edit),
);
const M3ETextField(
label: 'Email',
variant: M3ETextFieldVariant.outlined,
errorText: 'Enter a valid email address',
);
M3ESearchBar / M3ESearchAnchor
Search field, or a bar that opens a full search view. Escape closes the view. Enter or Space on the anchor opens it.
// Inline bar
M3ESearchBar(
controller: searchController,
hintText: 'Search components',
trailing: [
M3EIconButton(
icon: const Icon(M3EIcons.close),
onPressed: searchController.clear,
),
],
);
// Anchor + search view
final controller = M3ESearchController();
M3ESearchAnchor.bar(
searchController: controller,
barHintText: 'Search',
suggestionsBuilder: (context, controller) sync* {
for (final name in names.where((n) => n.contains(controller.text))) {
yield ListTile(
title: Text(name),
onTap: () => controller.closeView(name),
);
}
},
);
Keyboard: Enter or Space opens the view. Escape closes it.
Modal surfaces
Several components present transient UI over the app. They all require a
BuildContext with a Navigator / Overlay ancestor (any MaterialApp or
WidgetsApp provides this):
| Component | API |
|---|---|
M3EDialog |
M3EDialog.show, M3EDialog.showFullScreen |
M3EBottomSheet |
M3EBottomSheet.show |
M3ESideSheet |
M3ESideSheet.show |
M3ESnackbar |
M3ESnackbar.show |
Example app (detailed)
Live web build: paadevelopments.github.io/material_3_expressive.
The example/ project is a full gallery app:
- Entry point:
example/lib/main.dart—M3EMaterialAppwithautoTheming,dynamicColoring, and a five-tab catalog-driven gallery shell. The home app bar palette action openstheme_config_page.dartto toggle auto theming and dynamic color, pick one of five seed colors when dynamic color is off, and choose a font family (Google Sans Flex default, system, Roboto Flex, or Roboto Mono). Open View → Typography for type scale, variable-font axes, and conversion demos. - Pages: playgrounds under
example/lib/pages/playground/, grouped by tab (do/,pick/,view/,nav/,find/). - Theme toggle: app-bar
M3EIconButtoncallsM3ETheme.controllerOf(context)?.toggleBrightness(...).
cd example
flutter pub get
flutter run
Pick a device or simulator when prompted. Use the bottom navigation bar to switch between component groups, the palette icon for theme settings, and the brightness icon to toggle light/dark mode.
Development
Static analysis and tests:
flutter analyze
flutter test
Custom lint rules live in tools/klin_dart (path dependency, omitted from the published archive):
export PATH="$PWD/.fvm/flutter_sdk/bin:$PATH"
dart run custom_lint
Support
If this package helps your project:
- Star it on pub.dev
- Star or fork the repo on GitHub
- Share it with others building Material 3 Expressive UIs
Version 1.1.1 — API compatibility
Version 1.1.1 adds typography foundations (M3ETypography, variable-font
axes, style conversion) and exports buildM3EThemeDefaults() and
M3EDynamicColorHost through the public barrel. These changes are additive:
M3EThemeData.typeScale remains a baseline alias, and existing component code
continues to work without migration.
Credits
Several components were ported or vendored from earlier Material 3 Expressive implementations. Thanks to the original authors:
| Author | Components | Source |
|---|---|---|
| Mudit Purohit | Buttons, split buttons, button groups | m3e_buttons |
| Mudit Purohit | Dropdown menus | m3e_dropdown_menu |
| Mudit Purohit | Expandable lists | m3e_expandable |
| Emily | Icon buttons | icon_button_m3e |
| Emily | Navigation bar | navigation_bar_m3e |
| Emily | Navigation rail | navigation_rail_m3e |
| Emily | Loading indicator (Flutter package) | loading_indicator_m3e |
| The Android Open Source Project | Loading indicator (Compose reference) | LoadingIndicator.kt |
| The Android Open Source Project | Slider / RangeSlider / VerticalSlider (Compose reference, material3:1.4.0-alpha01) |
Slider.kt / SliderTokens.kt |
| The Android Open Source Project | Linear / circular wavy progress (Compose reference) | LinearWavyProgressIndicator / CircularWavyProgressIndicator |
| The Android Open Source Project | Floating / docked toolbars (Compose reference, material3:1.4.0-alpha01) |
FloatingToolbar.kt / FlexibleBottomAppBar / DockedToolbarTokens |
| The Flutter Authors | Carousel view layout (CarouselView) |
Flutter SDK / m3_carousel |
| pub.dev | Spring motion (motor), expressive morph polygons (material_new_shapes), dynamic color (dynamic_color) |
See Dependencies |
Copyright notices and licenses from those sources are retained in the
corresponding source files where applicable, and summarized in
NOTICE.
License
Distributed under the MIT License. See LICENSE for details.
Copyright (c) 2026 Paa Developments paa.code.me@gmail.com
Libraries
- components/app_bars/components/m3e_app_bar_semantics
- components/app_bars/controllers/m3e_app_bar_controller
- components/app_bars/enums/m3e_app_bar_enums
- components/app_bars/m3e_app_bars
- components/app_bars/styles/m3e_app_bar_theme
- components/badges/components/m3e_badge_layout
- components/badges/enums/m3e_badge_alignment
- components/badges/m3e_badges
- components/badges/styles/m3e_badge_theme
- components/bottom_sheets/m3e_bottom_sheets
- components/bottom_sheets/styles/m3e_bottom_sheet_theme
- components/buttons/components/m3e_no_overflow_strategy
- components/buttons/components/m3e_overflow_strategy
- components/buttons/components/m3e_radius_and_padding_motion
- components/buttons/components/m3e_scroll_overflow_strategy
- components/buttons/styles/m3e_overflow_bottom_sheet_decoration
- components/buttons/styles/m3e_overflow_popup_decoration
- components/cards/components/m3e_card_container_transform
- components/cards/components/m3e_card_group
- components/cards/components/m3e_card_group_scope
- components/cards/controllers/m3e_card_controller
- components/cards/enums/m3e_card_divider_span
- components/cards/enums/m3e_card_group_layout
- components/cards/enums/m3e_card_overflow_alignment
- components/cards/enums/m3e_card_swipe_mode
- components/cards/enums/m3e_card_variant
- components/cards/m3e_cards
- components/cards/styles/m3e_card_theme
- components/carousel/components/m3e_carousel_item
- components/carousel/components/m3e_carousel_show_all
- components/carousel/components/m3e_carousel_slot_scope
- components/carousel/components/m3e_carousel_step_physics
- components/carousel/components/m3e_carousel_track_clip
- components/carousel/components/m3e_carousel_view
- components/carousel/components/m3e_carousel_wrapper
- components/carousel/enums/m3e_carousel_type
- components/carousel/m3e_carousel
- components/carousel/models/m3e_carousel_change_details
- components/carousel/models/m3e_carousel_scrim
- components/carousel/styles/m3e_carousel_theme
- components/carousel/utils/m3e_carousel_layout
- components/carousel/utils/m3e_carousel_scroll_helper
- components/checkbox/m3e_checkbox
- components/checkbox/styles/m3e_checkbox_theme
- components/chips/enums/m3e_chip_type
- components/chips/m3e_chips
- components/chips/styles/m3e_chip_theme
- components/date_pickers/components/m3e_calendar_date_range_picker
- components/date_pickers/components/m3e_date_picker_actions
- components/date_pickers/components/m3e_date_picker_dialog_content
- components/date_pickers/components/m3e_date_picker_header
- components/date_pickers/components/m3e_date_picker_mode_toggle
- components/date_pickers/components/m3e_day_cell
- components/date_pickers/components/m3e_day_picker
- components/date_pickers/components/m3e_input_date_picker_form_field
- components/date_pickers/components/m3e_input_date_range_picker_form_field
- components/date_pickers/components/m3e_month_picker
- components/date_pickers/components/m3e_year_picker
- components/date_pickers/enums/m3e_date_picker_enums
- components/date_pickers/m3e_calendar_date_picker
- components/date_pickers/m3e_date_picker_dialog
- components/date_pickers/m3e_date_pickers
- components/date_pickers/m3e_date_range_picker_dialog
- components/date_pickers/models/m3e_calendar_labels
- components/date_pickers/models/m3e_date_picker_models
- components/date_pickers/res/m3e_date_picker_constants
- components/date_pickers/styles/m3e_date_picker_theme
- components/date_pickers/utils/m3e_date_picker_utils
- components/dialogs/components/m3e_dialog_inset
- components/dialogs/m3e_dialogs
- components/dialogs/styles/m3e_dialog_theme
- components/divider/enums/m3e_divider_axis
- components/divider/enums/m3e_divider_inset
- components/divider/m3e_divider
- components/divider/styles/m3e_divider_theme
- components/dropdown_menus/components/m3e_dropdown_chips
- components/dropdown_menus/controllers/m3e_dropdown_controller
- components/dropdown_menus/enums/m3e_dropdown_expand_direction
- components/dropdown_menus/models/m3e_dropdown_item
- components/dropdown_menus/styles/m3e_dropdown_chip_style
- components/dropdown_menus/styles/m3e_dropdown_field_style
- components/dropdown_menus/styles/m3e_dropdown_item_style
- components/dropdown_menus/styles/m3e_dropdown_panel_style
- components/dropdown_menus/styles/m3e_dropdown_search_style
- components/dropdown_menus/utils/m3e_dropdown_spring_motion
- components/extended_fabs/controllers/m3e_extended_fab_controller
- components/extended_fabs/enums/m3e_extended_fab
- components/extended_fabs/m3e_extended_fabs
- components/floating_action_buttons/components/m3e_fab_container_transform
- components/floating_action_buttons/controllers/m3e_fab_controller
- components/floating_action_buttons/enums/m3e_fab
- components/floating_action_buttons/styles/m3e_fab_decoration
- components/floating_action_buttons/styles/m3e_fab_theme
- components/lists/components/m3e_card_list_item
- components/lists/components/m3e_card_radius_motion
- components/lists/components/m3e_expandable_builders
- components/lists/components/m3e_expandable_data
- components/lists/components/m3e_expandable_expanded
- components/lists/components/m3e_expandable_header_tap_scope
- components/lists/components/m3e_expandable_item
- components/lists/components/m3e_expandable_nest_scope
- components/lists/components/m3e_expandable_snap_collapse
- components/lists/components/m3e_expandable_sublist
- components/lists/components/m3e_list_avatar
- components/lists/components/m3e_list_drag_proxy_scope
- components/lists/components/m3e_list_feature_host
- components/lists/components/m3e_list_feature_scope
- components/lists/components/m3e_list_focus_ring
- components/lists/components/m3e_list_image
- components/lists/components/m3e_list_interaction
- components/lists/components/m3e_list_item_scope
- components/lists/components/m3e_list_key_target
- components/lists/components/m3e_list_keyboard
- components/lists/components/m3e_list_reorder_exclude
- components/lists/components/m3e_list_reorder_host
- components/lists/components/m3e_list_reorder_session_scope
- components/lists/components/m3e_list_row_surface
- components/lists/components/m3e_list_swipe_overflow
- components/lists/components/m3e_list_trailing_override
- components/lists/components/m3e_list_transform_publisher
- components/lists/components/m3e_list_transform_scope
- components/lists/components/m3e_list_video
- components/lists/controllers/m3e_dismissible_card_controller
- components/lists/controllers/m3e_dismissible_list_controller
- components/lists/controllers/m3e_expandable_list_controller
- components/lists/enums/m3e_expandable_enums
- components/lists/enums/m3e_list_enums
- components/lists/enums/m3e_list_selection_enums
- components/lists/enums/m3e_list_swipe_edge
- components/lists/enums/m3e_list_swipe_mode
- components/lists/m3e_lists
- components/lists/models/m3e_dismissible_slot
- components/lists/models/m3e_list_item_swipe
- components/lists/models/m3e_list_swipe_action
- components/lists/styles/m3e_dismissible_list_style
- components/lists/styles/m3e_expandable_style
- components/lists/styles/m3e_list_card_list_theme
- components/lists/styles/m3e_list_dismissible_theme
- components/lists/styles/m3e_list_expandable_theme
- components/lists/styles/m3e_list_item_theme
- components/lists/styles/m3e_list_reorder_state
- components/lists/styles/m3e_list_selection_state
- components/lists/styles/m3e_list_theme
- components/lists/utils/m3e_expandable_spring_motion
- components/lists/utils/m3e_list_immediate_tap
- components/lists/utils/m3e_list_row_features
- components/lists/utils/m3e_list_selection_binding
- components/lists/utils/m3e_list_selection_fill
- components/lists/utils/m3e_measure_size
- components/loading_indicator/components/m3e_expressive_loading_indicator
- components/loading_indicator/enums/m3e_loading_indicator_variant
- components/loading_indicator/m3e_loading_indicator
- components/loading_indicator/styles/m3e_loading_indicator_theme
- components/navigation_rail/components/m3e_rail_badge_view
- components/navigation_rail/components/m3e_rail_item
- components/progress_indicators/components/m3e_circular_progress_painter
- components/progress_indicators/components/m3e_circular_wavy_progress_painter
- components/progress_indicators/components/m3e_linear_progress_painter
- components/progress_indicators/enums/m3e_progress_enums
- components/progress_indicators/m3e_progress_indicators
- components/progress_indicators/styles/m3e_progress_indicator_theme
- components/progress_indicators/utils/m3e_progress_indicator_utils
- components/radio_button/styles/m3e_radio_theme
- components/refresh_indicator/controllers/m3e_refresh_indicator_controller
- components/refresh_indicator/enums/m3e_refresh_status
- components/refresh_indicator/m3e_refresh_indicator
- components/refresh_indicator/styles/m3e_refresh_indicator_theme
- components/search/components/m3e_search_view
- components/search/controllers/m3e_search_controller
- components/search/m3e_search
- components/search/m3e_search_anchor
- components/search/m3e_search_bar
- components/search/res/m3e_search_constants
- components/search/styles/m3e_search_bar_theme
- components/search/styles/m3e_search_view_theme
- components/segmented_buttons/components/m3e_segment_divider
- components/segmented_buttons/models/m3e_segment
- components/selection/components/m3e_selection_app_bar
- components/selection/components/m3e_selection_flip
- components/selection/components/m3e_selection_leading
- components/selection/components/m3e_selection_scope
- components/selection/controllers/m3e_selection_controller
- components/selection/m3e_selection
- components/selection/styles/m3e_selection_theme
- components/side_sheets/m3e_side_sheets
- components/side_sheets/styles/m3e_side_sheet_theme
- components/sliders/components/m3e_range_slider_track
- components/sliders/components/m3e_slider_centered_track
- components/sliders/components/m3e_slider_dot_overlay
- components/sliders/components/m3e_slider_thumb
- components/sliders/components/m3e_slider_track
- components/sliders/components/m3e_slider_track_painter
- components/sliders/components/m3e_slider_value_indicator
- components/sliders/enums/m3e_slider_enums
- components/sliders/m3e_range_slider
- components/sliders/m3e_sliders
- components/sliders/models/m3e_slider_dot_builder
- components/sliders/models/m3e_slider_range
- components/sliders/models/m3e_slider_range_labels
- components/sliders/models/m3e_slider_track_icons
- components/sliders/res/m3e_slider_tokens
- components/sliders/styles/m3e_slider_theme
- components/sliders/utils/m3e_slider_dot_geometry
- components/sliders/utils/m3e_slider_dot_layout
- components/sliders/utils/m3e_slider_math
- components/sliders/utils/m3e_slider_track_paint_metrics
- components/snackbar/components/m3e_snackbar_host
- components/snackbar/controllers/m3e_snackbar_controller
- components/snackbar/m3e_snackbar
- components/snackbar/styles/m3e_snackbar_theme
- components/switch_control/m3e_switch_control
- components/switch_control/styles/m3e_switch_theme
- components/tabs/controllers/m3e_tabs_controller
- components/tabs/enums/m3e_tabs_alignment
- components/tabs/enums/m3e_tabs_variant
- components/tabs/m3e_tabs
- components/tabs/m3e_tabs_view
- components/tabs/models/m3e_tab
- components/tabs/styles/m3e_tab_theme
- components/text_fields/enums/m3e_text_field_variant
- components/text_fields/m3e_text_fields
- components/text_fields/styles/m3e_text_field_theme
- components/time_pickers/components/m3e_day_period_control
- components/time_pickers/components/m3e_dial_time_picker
- components/time_pickers/components/m3e_hour_minute_text_field
- components/time_pickers/components/m3e_input_time_picker_form_field
- components/time_pickers/components/m3e_time_dial_painter
- components/time_pickers/components/m3e_time_picker_actions
- components/time_pickers/components/m3e_time_picker_dialog_content
- components/time_pickers/components/m3e_time_picker_header
- components/time_pickers/enums/m3e_time_picker_enums
- components/time_pickers/m3e_time_picker_dialog
- components/time_pickers/m3e_time_pickers
- components/time_pickers/models/m3e_time
- components/time_pickers/res/m3e_time_picker_constants
- components/time_pickers/styles/m3e_time_picker_theme
- components/time_pickers/utils/m3e_time_picker_utils
- components/toolbars/components/m3e_toolbar_actions_row
- components/toolbars/components/m3e_toolbar_body
- components/toolbars/components/m3e_toolbar_expanding_actions
- components/toolbars/components/m3e_toolbar_fab_layout
- components/toolbars/components/m3e_toolbar_fab_slot
- components/toolbars/components/m3e_toolbar_measure_size
- components/toolbars/components/m3e_toolbar_title_block
- components/toolbars/controllers/m3e_toolbar_visibility_controller
- components/toolbars/enums/m3e_toolbar_enums
- components/toolbars/m3e_toolbar_scroll_behavior
- components/toolbars/m3e_toolbars
- components/toolbars/models/m3e_toolbar_item
- components/toolbars/res/m3e_toolbar_tokens
- components/toolbars/styles/m3e_toolbar_theme
- components/toolbars/utils/m3e_toolbar_item_layout
- components/toolbars/utils/m3e_toolbar_spring_motion
- components/tooltips/controllers/m3e_tooltip_controller
- components/tooltips/enums/m3e_tooltip_placement
- components/tooltips/m3e_tooltips
- components/tooltips/styles/m3e_tooltip_theme
- components/tooltips/utils/m3e_tooltip_position_delegate
- foundations/color/m3e_color_scheme
- foundations/color/m3e_color_utils
- foundations/components/m3e_component_theme
- foundations/foundations
- Design tokens, theming hosts, and shared interaction primitives for Material 3 Expressive components.
- foundations/interaction/m3e_focus
- foundations/interaction/m3e_focus_interaction
- foundations/interaction/m3e_focus_ring
- foundations/interaction/m3e_focus_ring_theme
- foundations/interaction/m3e_haptics
- foundations/interaction/m3e_ink_splash_theme
- foundations/interaction/m3e_motion
- foundations/interaction/m3e_overlay_history
- foundations/interaction/m3e_state_layer
- foundations/interaction/m3e_state_layer_overlay
- foundations/interaction/m3e_tappable
- foundations/interaction/m3e_tappable_ink_scope
- foundations/m3e_icons
- foundations/shape/m3e_material_new_shapes_bridge
- foundations/shape/m3e_shape_clipper
- foundations/shape/m3e_shape_container
- foundations/shape/m3e_shape_kind
- foundations/shape/m3e_shapes
- foundations/theme/m3e_dynamic_color_host
- foundations/theme/m3e_material_app
- foundations/theme/m3e_resolved_theme
- foundations/theme/m3e_theme
- foundations/theme/m3e_theme_controller
- foundations/theme/m3e_theme_data
- foundations/theme/m3e_theme_defaults
- foundations/theme/m3e_theme_extension
- foundations/theme/m3e_theme_scope
- foundations/tokens/m3e_dimensions
- foundations/tokens/m3e_elevation
- foundations/tokens/m3e_safe_area
- foundations/tokens/m3e_scrim_system_ui
- foundations/tokens/m3e_spacing
- foundations/type/m3e_type_style_conversion
- foundations/type/m3e_type_style_tokens
- foundations/type/m3e_typography
- foundations/type/m3e_variable_font_axes
- foundations/type/m3e_variable_font_config
- material_3_expressive
- Material 3 Expressive: a faithful Flutter implementation of the Material 3
component set, exposed as direct
M3E*component widgets.




