Material 3 design
Material 3 design
In the previous chapters we built the Travel Notes app and cleaned up the structure. The app works, but we can still make the UI more consistent.
Flutter uses Material Design as its default design system. Modern Flutter apps use Material 3, the current version of Google's design system.
Material 3 is not only about colors. It also gives guidance for typography, spacing, components, dark mode, shapes and motion.
Use a Travel Notes theme
We already created a theme in main.dart. Now we can make it a little more complete.
Replace the theme property in MaterialApp with this:
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.teal,
),
appBarTheme: AppBarTheme(
backgroundColor: colors.primary,
foregroundColor: colors.onPrimary,
),
useMaterial3: true,
),
Add a dark theme too:
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.teal,
brightness: Brightness.dark,
),
useMaterial3: true,
),
themeMode: ThemeMode.system,
The full MaterialApp now looks like this:
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Travel Notes',
theme: ThemeData(
colorScheme: colors,
appBarTheme: AppBarTheme(
backgroundColor: colors.primary,
foregroundColor: colors.onPrimary,
),
useMaterial3: true,
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.teal,
brightness: Brightness.dark,
),
useMaterial3: true,
),
themeMode: ThemeMode.system,
home: const AuthGate(),
);
seedColor is the base color. Flutter generates a complete color scheme from it. For Travel Notes, teal fits the calm travel/journal feeling better than a random default color.
Use theme colors
Inside widgets, use colors from the theme instead of hardcoded colors.
final ColorScheme colors = Theme.of(context).colorScheme;
The TripCard from the architecture chapter already does this:
leading: CircleAvatar(
backgroundColor: colors.primary,
foregroundColor: colors.onPrimary,
child: Text(trip.destination.substring(0, 1)),
),
The on... colors are designed to be readable on top of the matching background color.
| Color | Use for |
|---|---|
primary | important actions and active elements |
secondary | supporting actions |
surface | app backgrounds, cards and sheets |
error | validation and destructive feedback |
onPrimary | text/icons shown on primary |
onSurface | text/icons shown on surface |
Typography
Use the text theme instead of hardcoding every font size.
For example, the empty state in TripListPage can use the theme:
Text(
"No trips yet",
style: Theme.of(context).textTheme.titleMedium,
)
Common text styles:
| Style | Use for |
|---|---|
headlineLarge | important page titles |
headlineMedium | section titles |
titleLarge | app bars, dialogs, cards |
titleMedium | empty states and important list text |
bodyLarge | main readable text |
bodyMedium | supporting text |
labelLarge | buttons and labels |
Material 3 components
Prefer modern Material 3 components:
| Older pattern | Material 3 pattern |
|---|---|
| custom filled button | FilledButton or FilledButton.icon |
| custom outlined button | OutlinedButton |
| custom floating panel | BottomSheet |
| old bottom navigation | NavigationBar |
| custom side menu | NavigationRail |
In TripDetailPage, the save button from the forms chapter already uses a Material 3 component:
FilledButton.icon(
onPressed: isSaving ? null : _saveTrip,
icon: const Icon(Icons.save),
label: const Text("Save trip"),
)
This is better than manually styling a button with custom colors, because it automatically follows the app theme.
Spacing and shape
Keep spacing consistent. The Travel Notes pages already use 8, 12 and 16 a lot. That is fine for this course, but you can make the intent clearer with constants:
const double spacingSmall = 8;
const double spacingMedium = 12;
const double spacingLarge = 16;
For example:
Padding(
padding: const EdgeInsets.all(spacingLarge),
child: Form(...),
)
Do not overdo this in small apps. Constants are useful when they make the code easier to read, not when they make every line longer.
Dark mode
Because themeMode is set to ThemeMode.system, the app follows the user's device setting.
This only works well when pages use theme colors. If a widget hardcodes Colors.black, Colors.white or Colors.red, it might look wrong in dark mode.
For validation or error text, prefer:
Theme.of(context).colorScheme.error
instead of:
Colors.red
Best practices
- Start with
ColorScheme.fromSeed. - Use
Theme.of(context)instead of hardcoded styles. - Add dark mode early.
- Use Material components before building custom widgets.
- Keep spacing and border radius consistent.
- Test screens with light mode, dark mode and large text.
Exercise
Improve the Travel Notes UI:
- Add the dark theme to
main.dart. - Make sure
useMaterial3: trueis enabled. - Replace hardcoded error colors with
Theme.of(context).colorScheme.error. - Use
Theme.of(context).textThemefor the empty state inTripListPage. - Check the login page, trip list and trip detail page in light and dark mode.