buildThemeData function

ThemeData buildThemeData(
  1. BrandTokens tokens
)

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(),
      },
    ),
  );
}