buildThemeData function
Builds a Material ThemeData from the provided BrandTokens.
This is the core theme building function that converts design tokens
into Flutter's Material 3 theme.
Implementation
ThemeData buildThemeData(BrandTokens tokens) {
final colour = tokens.colour;
final shapes = tokens.shapes;
final componentColours = tokens.componentColours;
final typography = tokens.typography;
final colorScheme = ColorScheme(
// Core
brightness: Brightness.light,
// Primary colors
primary: colour.primary,
onPrimary: colour.onPrimary,
primaryContainer: colour.primaryContainer,
onPrimaryContainer: colour.onPrimaryContainer,
// Secondary colors
secondary: colour.secondary,
onSecondary: colour.onSecondary,
secondaryContainer: colour.secondaryContainer,
onSecondaryContainer: colour.onSecondaryContainer,
// Tertiary colors
tertiary: colour.tertiary,
onTertiary: colour.onTertiary,
tertiaryContainer: colour.tertiaryContainer,
onTertiaryContainer: colour.onTertiaryContainer,
// Error colors
error: colour.error,
onError: colour.onError,
errorContainer: colour.errorContainer,
onErrorContainer: colour.onErrorContainer,
// Surface colors
surface: colour.surface,
onSurface: colour.onSurface,
onSurfaceVariant: colour.onSurfaceVariant,
surfaceContainerHighest: colour.surfaceContainerHighest,
surfaceContainerHigh: colour.surfaceContainerHigh,
surfaceContainer: colour.surfaceContainer,
surfaceContainerLow: colour.surfaceContainerLow,
surfaceContainerLowest: colour.surfaceContainerLowest,
// Outline colors
outline: colour.outline,
outlineVariant: colour.outlineVariant,
// Inverse colors
inverseSurface: colour.inverseSurface,
onInverseSurface: colour.inverseOnSurface,
inversePrimary: colour.inversePrimary,
// Other properties
shadow: colour.onSurface,
surfaceTint: colour.surfaceTint,
scrim: colour.scrim,
);
final textTheme = _buildTextTheme(tokens.typography, colorScheme.onSurface);
return ThemeData(
// Core theme properties
useMaterial3: true,
colorScheme: colorScheme,
textTheme: textTheme,
scaffoldBackgroundColor: colorScheme.surface,
// Surface-related themes
cardTheme: CardThemeData(
color: colorScheme.surface,
elevation: tokens.elevation.medium,
shadowColor: colorScheme.shadow,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(tokens.shapes.cornerSmall),
),
),
bottomSheetTheme: BottomSheetThemeData(
backgroundColor: colorScheme.surface,
),
drawerTheme: DrawerThemeData(
backgroundColor: colorScheme.secondaryContainer,
// Fixed nav drawer width — a component layout dimension, deliberately not
// a design token (no matching rung on the size scale).
width: 250,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topRight: Radius.circular(shapes.cornerNone),
bottomRight: Radius.circular(shapes.cornerNone),
),
),
),
popupMenuTheme: PopupMenuThemeData(
color: colorScheme.surface,
iconSize: tokens.spacing.spaceMd3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(shapes.cornerXs),
),
textStyle: typography.bodyMediumRegular.toTextStyle(
color: componentColours.textDefault,
),
),
expansionTileTheme: ExpansionTileThemeData(
iconColor: colorScheme.onSurface,
collapsedIconColor: colorScheme.onSurface,
collapsedBackgroundColor: colorScheme.surface,
backgroundColor: colorScheme.surface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(shapes.cornerNone),
),
),
dropdownMenuTheme: DropdownMenuThemeData(
menuStyle: MenuStyle(
backgroundColor: WidgetStateProperty.all(colorScheme.surface),
elevation: WidgetStateProperty.all(tokens.elevation.low),
),
inputDecorationTheme: _buildInputDecoration(colorScheme, tokens).copyWith(
floatingLabelBehavior: FloatingLabelBehavior.always,
hintStyle: tokens.typography.label.toTextStyle(
color: tokens.componentColours.textDefault,
),
),
textStyle: typography.bodyMediumRegular.toTextStyle(
color: componentColours.textDefault,
),
disabledColor: componentColours.textDisabled,
),
// Button themes
elevatedButtonTheme: ElevatedButtonThemeData(
style: ButtonStyle(
elevation: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.disabled)) {
return tokens.elevation.none;
}
return tokens.elevation.low;
}),
minimumSize: WidgetStateProperty.all(
Size(tokens.spacing.spaceXl1, tokens.spacing.spaceLg5),
),
mouseCursor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.disabled)) {
return SystemMouseCursors.forbidden;
}
if (states.contains(WidgetState.pressed)) {
return SystemMouseCursors.click;
}
if (states.contains(WidgetState.hovered)) {
return SystemMouseCursors.click;
}
return SystemMouseCursors.basic;
}),
textStyle: WidgetStateProperty.all(
tokens.typography.buttonSemiBold.toTextStyle(),
),
foregroundColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.disabled)) {
return tokens.componentColours.buttonFilledLabelDisabled;
}
if (states.contains(WidgetState.pressed)) {
return tokens.componentColours.buttonFilledLabelPressed;
}
if (states.contains(WidgetState.hovered)) {
return tokens.componentColours.buttonFilledLabelHover;
}
return tokens.componentColours.buttonFilledLabelEnabled;
}),
backgroundColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.disabled)) {
return tokens.componentColours.buttonFilledDisabled;
}
if (states.contains(WidgetState.pressed)) {
return tokens.componentColours.buttonFilledContainerPressed;
}
if (states.contains(WidgetState.hovered)) {
return tokens.componentColours.buttonFilledContainerHover;
}
return tokens.componentColours.buttonFilledContainerEnabled;
}),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: ButtonStyle(
elevation: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.disabled)) {
return tokens.elevation.none;
}
return tokens.elevation.low;
}),
textStyle: WidgetStateProperty.all(
tokens.typography.buttonRegular.toTextStyle(),
),
side: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.disabled)) {
return BorderSide(
color: tokens.componentColours.buttonOutlineDisabled,
);
}
if (states.contains(WidgetState.pressed)) {
return BorderSide(
color: tokens.componentColours.buttonOutlineContainerPressed,
);
}
if (states.contains(WidgetState.hovered)) {
return BorderSide(
color: tokens.componentColours.buttonOutlineContainerHover,
);
}
return BorderSide(
color: tokens.componentColours.buttonOutlineContainerEnabled,
);
}),
foregroundColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.disabled)) {
return tokens.componentColours.buttonOutlineLabelDisabled;
}
if (states.contains(WidgetState.pressed)) {
return tokens.componentColours.buttonOutlineLabelPressed;
}
return tokens.componentColours.buttonOutlineLabelEnabled;
}),
backgroundColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.hovered)) {
return tokens.componentColours.buttonOutlineContainerHover;
}
return null;
}),
),
),
textButtonTheme: TextButtonThemeData(
style: ButtonStyle(
elevation: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.disabled)) {
return tokens.elevation.none;
}
return tokens.elevation.low;
}),
textStyle: WidgetStateProperty.all(
tokens.typography.buttonRegular.toTextStyle(
color: tokens.componentColours.buttonTextLabelEnabled,
),
),
foregroundColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.disabled)) {
return tokens.componentColours.buttonTextLabelDisabled;
}
if (states.contains(WidgetState.pressed)) {
return tokens.componentColours.buttonTextLabelPressed;
}
if (states.contains(WidgetState.hovered)) {
return tokens.componentColours.buttonTextLabelHover;
}
return tokens.componentColours.buttonTextLabelEnabled;
}),
),
),
iconButtonTheme: IconButtonThemeData(
style: IconButton.styleFrom(
padding: EdgeInsets.zero,
iconSize: tokens.spacing.spaceMd2,
),
),
// Input and form themes
inputDecorationTheme: _buildInputDecoration(colorScheme, tokens),
checkboxTheme: CheckboxThemeData(
fillColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.disabled)) {
return null; // Use default disabled appearance
}
if (states.contains(WidgetState.selected)) {
return colorScheme.onSurface;
}
return Colors.transparent;
}),
checkColor: WidgetStateProperty.all(tokens.componentColours.iconWhite),
side: BorderSide(
color: colorScheme.onSurface,
width: tokens.spacing.spaceXs1,
),
),
radioTheme: RadioThemeData(
fillColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.disabled)) {
return componentColours
.iconDisabled; // Use default disabled appearance
}
if (states.contains(WidgetState.selected)) {
return colorScheme.onSurface;
}
return colorScheme.onSurface;
}),
),
// Navigation themes
appBarTheme: AppBarTheme(
backgroundColor: colorScheme.surface,
foregroundColor: colorScheme.onSurface,
elevation: tokens.elevation.none,
centerTitle: false,
titleTextStyle: tokens.typography.titleLarge.toTextStyle(
color: colorScheme.onSurface,
),
iconTheme: IconThemeData(color: colorScheme.surface),
),
snackBarTheme: SnackBarThemeData(
showCloseIcon: true,
closeIconColor: colorScheme.surface,
contentTextStyle: TextStyle(color: colorScheme.onSurface),
behavior: SnackBarBehavior.fixed,
),
// Other component themes
chipTheme: ChipThemeData(
backgroundColor: colorScheme.surfaceContainerLow,
disabledColor: tokens.componentColours.chipsDisabledContainer,
selectedColor: tokens.componentColours.chipSelectedContainer,
labelStyle: tokens.typography.bodySmallMedium.toTextStyle(
color: tokens.componentColours.textDefault,
),
secondaryLabelStyle: tokens.typography.bodySmallMedium.toTextStyle(
color: tokens.componentColours.textPrimary,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(tokens.shapes.cornerLarge),
side: BorderSide(
color: tokens.componentColours.chipsEnabledOutline,
width: tokens.spacing.spaceXs1,
),
),
),
dividerTheme: DividerThemeData(
color: colorScheme.outlineVariant,
thickness: 1,
),
iconTheme: IconThemeData(color: tokens.componentColours.iconPrimary),
progressIndicatorTheme: ProgressIndicatorThemeData(
color: colorScheme.primary,
),
// Transitions
pageTransitionsTheme: PageTransitionsTheme(
builders: {
TargetPlatform.android: NoTransitionsBuilder(),
TargetPlatform.iOS: NoTransitionsBuilder(),
TargetPlatform.linux: NoTransitionsBuilder(),
TargetPlatform.macOS: NoTransitionsBuilder(),
TargetPlatform.windows: NoTransitionsBuilder(),
},
),
);
}