material_3_expressive 1.0.4
material_3_expressive: ^1.0.4 copied to clipboard
A faithful Flutter implementation of the Material 3 Expressive component set, exposed as direct M3E component widgets.
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 #
An interactive gallery demonstrating all 44 widgets lives in the
example/ directory. It groups components the same way as the
official Material 3 catalog:
| Tab | Page | Components |
|---|---|---|
| Do | actions_page.dart |
Buttons, FABs, groups, toggles, segmented & split buttons |
| Pick | selection_page.dart |
Checkbox, radio, switch, chips, dropdown, slider (incl. wavy), pickers |
| View | containment_page.dart |
Cards, carousel, lists, divider, dialogs, sheets |
| Nav | navigation_page.dart |
App bars (incl. search), tabs, nav bar/rail/drawer, toolbar, menu |
| Find | feedback_page.dart |
Badges, progress, refresh, tooltip, snackbar, inputs |
The gallery shell in example/lib/main.dart uses
M3EMaterialApp with adaptive theming and a light/dark toggle.
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. - Expressive motion & interaction — spring physics (via
motor), shape morphing, liquid 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), elevation, haptics, and state layers via theM3EThemeinherited widget. - Interactive example gallery in
example/with live demos for every component.
Requirements #
| Tool | Version |
|---|---|
| Flutter | >= 3.3.0 |
| Dart | ^3.12.0 |
Installation #
Add the package to your pubspec.yaml:
dependencies:
material_3_expressive: ^1.0.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, and Material primitives used throughout |
collection |
Small collection helpers used by component logic |
dynamic_color |
Platform dynamic / Material You seed colors for M3EMaterialApp (dynamicColoring) |
motor |
Unified motion API — physics springs and curves that drive expressive morphs and liquid selection indicators |
material_new_shapes |
Expressive RoundedPolygon morph shapes (M3EMaterialNewShapes) 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.
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 rolestypeScale—M3ETypeScale(display, headline, title, label, body)spacing,visualDensity, per-component*Themeextensions
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).
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/actions_page.dart(Do tab)
M3EButton
Five color variants with shape morphing on press.
M3EButton(
style: M3EButtonStyle.elevated,
onPressed: () {},
child: const Text('Elevated'),
);
M3EButton.icon(
icon: const Icon(M3EIcons.add),
label: const Text('Add'),
onPressed: () {},
);
M3EIconButton
Icon-only actions; supports toggle selection. Optional visualSize overrides
the painted control size while hit target follows theme rules.
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),
);
M3EFab
Floating action button in three sizes.
M3EFab(
icon: const Icon(M3EIcons.add),
size: M3EFabSize.large,
color: M3EFabColor.tertiary,
onPressed: () {},
);
M3EExtendedFab
FAB with a text label.
M3EExtendedFab(
label: 'Compose',
icon: const Icon(M3EIcons.edit),
onPressed: () {},
);
M3EFabMenu
Speed-dial menu anchored to a FAB. The FAB morphs size (80↔56) and circle when
opening; use position for left/right anchor, and optional expandIcon /
collapseIcon (fallbacks: icon / closeIcon).
M3EFabMenu(
position: M3EFabMenuPosition.right,
expandIcon: const Icon(M3EIcons.add),
collapseIcon: const Icon(M3EIcons.close),
items: [
M3EFabMenuItem(
icon: const Icon(M3EIcons.edit),
label: 'Note',
onPressed: () {},
),
M3EFabMenuItem(
icon: const Icon(M3EIcons.schedule),
label: 'Reminder',
onPressed: () {},
),
],
);
M3EButtonGroup
Grouped icon buttons with neighbour squish or connected corner morphing.
// in State
M3EButtonGroup(
selectedIndex: groupIndex,
onSelectedIndexChanged: (i) => setState(() => groupIndex = i ?? groupIndex),
actions: const [
M3EButtonGroupAction(icon: Icon(M3EIcons.arrow_back)),
M3EButtonGroupAction(icon: Icon(M3EIcons.add)),
M3EButtonGroupAction(icon: Icon(M3EIcons.arrow_forward)),
],
);
M3EButtonGroup(
type: M3EButtonGroupType.connected,
actions: const [
M3EButtonGroupAction(icon: Icon(M3EIcons.chevron_left)),
M3EButtonGroupAction(icon: Icon(M3EIcons.menu)),
M3EButtonGroupAction(icon: Icon(M3EIcons.chevron_right)),
],
);
M3EToggleButton
Toggle with round-to-square shape morphing.
// in State
M3EToggleButton.filled(
icon: const Icon(M3EIcons.favorite_border),
checkedIcon: const Icon(M3EIcons.favorite),
checked: isFavorite,
onCheckedChange: (v) => setState(() => isFavorite = v),
);
M3EToggleButton.text(
label: const Text('Bold'),
checked: isBold,
onCheckedChange: (v) => setState(() => isBold = v),
);
M3ESegmentedButton
Single- or multi-select segmented control.
// 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,
segments: const [
M3ESegment(value: 'new', label: 'New'),
M3ESegment(value: 'sale', label: 'Sale'),
],
selected: filters,
onSelectionChanged: (v) => setState(() => filters = v),
);
M3ESplitButton
Primary action with a trailing menu.
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')),
],
);
Selection #
See also:
example/lib/pages/selection_page.dart(Pick tab)
M3ECheckbox
Binary and tristate checkbox.
// in State
M3ECheckbox(
value: checked,
onChanged: (v) => setState(() => checked = v),
);
M3ECheckbox(
value: tristateValue,
tristate: true,
onChanged: (v) => setState(() => tristateValue = v),
);
M3ERadio
Mutually exclusive selection within a group. Optional [label] is part of the tap target.
// in State
M3ERadio<String>(
value: 'pro',
groupValue: plan,
label: const Text('Pro'),
onChanged: (v) => setState(() => plan = v),
);
M3ESwitch
On/off toggle with optional selected icon. Hover/focus/press paints a
thumb-centered translucent state layer (stateLayerSize, default 48 via theme).
// 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),
);
M3EChip
Assist, filter, input, and suggestion chip types.
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'),
);
M3EDropdownMenu
Static list, multi-select, search, and async loading.
// 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
M3EDropdownMenu<String>(
searchEnabled: true,
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) {},
);
M3ESlider
Compose Material 3 expressive slider — standard, centered, wavy, vertical, and
range. Optional trackThickness, cornerRadius, thumbLength, dotSize,
dotSpacing, and dotBuilder customize track, thumb, and stop/tick markers.
cornerRadius defaults to theme trackCornerRadius (8) and is not derived
from track thickness.
// in State
M3ESlider(
value: volume,
onChanged: (v) => setState(() => volume = 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),
),
);
M3EDatePicker
Dialog and inline calendar date pickers.
// 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),
);
M3ETimePicker
Dialog and dial-style time picker.
// Dialog
final M3ETime? picked = await M3ETimePicker.show(
context,
initialTime: time,
);
// Inline dial
M3EDialTimePicker(
value: time,
onChanged: (v) => setState(() => time = v),
);
Containment #
See also:
example/lib/pages/containment_page.dart(View tab)
M3ECard
Elevated, filled, and outlined surface for content and actions.
M3ECard(child: const Text('Elevated'));
M3ECard(
variant: M3ECardVariant.filled,
child: const Text('Filled'),
);
M3ECard(
variant: M3ECardVariant.outlined,
onPressed: () {},
child: const Text('Outlined (tap)'),
);
M3ECarousel
Hero, contained, and uncontained layouts — horizontal by default, or vertical
via axis.
M3ECarousel(
type: M3ECarouselType.hero,
heroAlignment: M3ECarouselHeroAlignment.center,
onTap: (index) {},
children: List.generate(10, (i) => ColoredBox(color: Colors.blue)),
);
M3ECarousel(
type: M3ECarouselType.contained,
isExtended: true,
children: items,
);
M3ECarousel(
type: M3ECarouselType.uncontained,
uncontainedItemExtent: 80,
children: items,
);
// Vertical (hero left/right map to top/bottom)
M3ECarousel(
axis: Axis.vertical,
type: M3ECarouselType.hero,
children: items,
);
M3EListItem
Standard list row with headline, supporting text, and slots.
M3EListItem(
headline: 'Wireless charging',
supportingText: 'On · Fast charge enabled',
leading: const Icon(M3EIcons.schedule),
trailing: const Icon(M3EIcons.chevron_right),
onTap: () {},
);
M3ECardList
Vertically stacked cards with dynamic corner rounding.
M3ECardList(
itemCount: 3,
onTap: (index) {},
itemBuilder: (context, index) => M3EListItem(
headline: 'Inbox',
leading: const Icon(M3EIcons.schedule),
),
);
// Scrollable / lazy
M3ECardList.builder(
itemCount: 20,
shrinkWrap: true,
itemBuilder: (context, index) => M3EListItem(
headline: 'Item $index',
),
);
M3EDismissibleColumn
Vertically swipeable card list with expressive physics.
M3EDismissibleColumn(
itemCount: 3,
onDismiss: (index, direction) async => true,
onTap: (index) {},
itemBuilder: (context, index) => M3EListItem(
headline: 'Swipe to dismiss',
leading: const Icon(M3EIcons.schedule),
),
);
M3EDismissibleList
Horizontal swipeable card list — same API as M3EDismissibleColumn.
SizedBox(
height: 120,
child: M3EDismissibleList(
itemCount: 5,
onDismiss: (index, direction) async => true,
itemBuilder: (context, index) => M3EListItem(
headline: 'Card $index',
),
),
);
M3EExpandableList
Expandable cards with expressive open/close motion.
M3EExpandableList(
data: [
M3EExpandableData(
title: 'Battery level low',
subtitle: 'Plug in your device.',
leading: const Icon(M3EIcons.battery_alert),
body: const Text('Your battery is at 10%.'),
),
],
);
// Scrollable variant for long lists
M3EExpandableList.scrollable(
data: expandableItems,
shrinkWrap: true,
);
// Sliver variant for CustomScrollView
CustomScrollView(
slivers: [
M3EExpandableList.sliver(data: expandableItems),
],
);
M3EDivider
Horizontal and vertical dividers.
const M3EDivider();
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/navigation_page.dart(Nav tab)
M3EAppBar
Top, search, sliver, and bottom app bar variants. 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
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: () {},
),
);
M3ETabs
Primary and secondary tab bars.
// 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)),
],
);
M3ENavigationBar
Bottom navigation for compact layouts.
// 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),
);
M3ENavigationRail
Vertical navigation for medium and expanded layouts.
// 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),
fab: M3ENavigationRailFabSlot(
icon: const Icon(M3EIcons.add),
label: 'Compose',
onPressed: () {},
),
);
M3ENavigationDrawer
Modal navigation drawer.
// in State
M3ENavigationDrawer(
headline: 'Mail',
destinations: const [
M3ENavigationDestination(icon: Icon(M3EIcons.menu), label: 'Home'),
M3ENavigationDestination(
icon: Icon(M3EIcons.search),
label: 'Search',
showBadge: true,
),
],
selectedIndex: drawerIndex,
onDestinationSelected: (i) => setState(() => drawerIndex = i),
);
M3EToolbar
Compose Material 3 expressive floating and docked toolbars. Floating toolbars
own expand/collapse when one action sets isExpandTrigger (expanded is the
initial state; the adjacent FAB stays visible and does not toggle expansion).
Optional visibilityController / scrollBehavior enable scroll-exit or manual
show/hide. Set onActiveIndexChanged for toolbar-managed action selection
(labeled actions animate width). Use fabExpandIcon / fabCollapseIcon when a
FAB morphs with the pill.
// 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: () {},
);
// 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: () {}),
],
);
// 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',
actions: <M3EToolbarItem>[
M3EToolbarAction(icon: M3EIcons.search, onPressed: () {}),
M3EToolbarAction(
icon: M3EIcons.delete,
label: 'Delete',
isDestructive: true,
onPressed: () {},
),
],
);
M3EMenu
Anchored dropdown menu. Top-level M3EMenuGroups each render as an elevated
surface with a gap between them; dividers stay inside a surface.
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: () {},
),
],
),
],
);
Feedback #
See also:
example/lib/pages/feedback_page.dart(Find tab)
M3EBadge
Notification dot or numeric badge on a child.
const M3EBadge(
showDot: true,
child: Icon(M3EIcons.menu, size: 28),
);
const M3EBadge(
count: 8,
child: Icon(M3EIcons.calendar_today, size: 28),
);
M3EProgressIndicator
Material 3 Expressive progress indicators with circular and linear variants,
including Compose-style wavy forms. Null value runs indeterminate animation
(classic linear: dual traveling segments with gaps; wavy linear/circular: m3e
style travel / spin+sweep; classic circular: spinning arc with gaps).
// Classic
const M3EProgressIndicator.circular();
M3EProgressIndicator.circular(value: 0.6);
const M3EProgressIndicator.linear();
SizedBox(
width: 200,
child: M3EProgressIndicator.linear(value: 0.6),
);
// 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
Expressive loading spinner.
const M3ELoadingIndicator();
const M3ELoadingIndicator(
variant: M3ELoadingIndicatorVariant.contained,
);
M3ERefreshIndicator
Pull-to-refresh wrapper for scrollables.
M3ERefreshIndicator(
onRefresh: () async {
await Future<void>.delayed(const Duration(seconds: 2));
},
child: ListView.builder(
itemCount: 12,
itemBuilder: (context, index) => Text('Item ${index + 1}'),
),
);
// Contained variant
M3ERefreshIndicator.contained(
onRefresh: () async {},
child: listView,
);
M3ETooltip
Descriptive label on hover or long-press.
M3ETooltip(
message: 'Compose a new message',
child: M3EIconButton(
icon: const Icon(M3EIcons.edit),
onPressed: () {},
),
);
M3ESnackbar
Brief feedback message — use .show (hosts via internal M3ESnackbarHost).
M3ESnackbar.show(
context,
message: 'Draft saved',
actionLabel: 'Undo',
onAction: () {},
);
M3ETextField
Filled and outlined text input with floating label.
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
Inline search field, or a bar that opens a full search view with suggestions.
When embedded in toolbars or app bars, expandOnFocus / expandRestPadding
control the horizontal inset spring on focus.
// 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),
);
}
},
);
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) #
The example/ project is a full gallery app:
- Entry point:
example/lib/main.dart—M3EMaterialAppwithautoTheming,dynamicColoring, and a five-tab gallery shell. - Pages: one file per Material 3 category under
example/lib/pages/. - 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 and the app-bar icon to toggle light/dark mode.
Development #
Static analysis and tests:
flutter analyze
flutter test
Optional custom lint rules (if klin_dart is enabled in your environment):
dart run custom_lint
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, toggle 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




