Flutter ThemeData: App-Wide Colours and Typography

Coding Liquids blog cover featuring Sagnik Bhattacharya for Flutter ThemeData: App-Wide Colours and Typography, with themedata supplied to materialapp, colorscheme.fromseed as the colour foundation, and texttheme and component themes.
Coding Liquids blog cover featuring Sagnik Bhattacharya for Flutter ThemeData: App-Wide Colours and Typography, with themedata supplied to materialapp, colorscheme.fromseed as the colour foundation, and texttheme and component themes.

Coding Liquids walkthroughs show one ThemeData change propagating across several Material controls.

Subscribe on YouTube@codingliquids

Centralise visual decisions in ThemeData

MaterialApp theme provides shared colour, typography, density, shape, and component defaults to descendants. A central theme makes redesigns and dark-mode parity tractable without editing every widget.

Hard-coded styling in feature screens slowly creates near-duplicate colours and radii.

Generate a coherent ColorScheme from a seed

ColorScheme.fromSeed derives primary, secondary, surface, error, and on-colours with appropriate tonal relationships. Choose brightness explicitly for light and dark schemes and override only brand-critical roles.

Using primary as a background without its matching onPrimary can break contrast.

@immutable
class StatusColours extends ThemeExtension<StatusColours> {
  const StatusColours({required this.success, required this.warning});
  final Color success;
  final Color warning;

  @override
  StatusColours copyWith({Color? success, Color? warning}) => StatusColours(
    success: success ?? this.success,
    warning: warning ?? this.warning,
  );

  @override
  StatusColours lerp(StatusColours? other, double t) => StatusColours(
    success: Color.lerp(success, other?.success, t)!,
    warning: Color.lerp(warning, other?.warning, t)!,
  );
}

final status = Theme.of(context).extension<StatusColours>()!;

Instagram’s theme-token map links semantic colour, typography, shape, and component state.

Follow me on Instagram@sagnikteaches

Define an intentional textTheme

TextTheme names roles such as headlineLarge, titleMedium, bodyLarge, and labelLarge rather than page-specific styles. Set font family, weight, height, and letter spacing at role level, then copyWith for exceptional emphasis.

Assigning arbitrary font sizes in each Text widget undermines scaling and hierarchy.

The Complete Flutter Guide course thumbnail

Build production-ready ThemeData features

The Complete Flutter Guide turns themedata into maintainable app architecture, polished UI, and testable production code.

Enrol now

Configure Material components once

appBarTheme, elevatedButtonTheme, cardTheme, and inputDecorationTheme remove repeated constructor styling. Use MaterialState or WidgetState-aware properties when pressed, disabled, selected, or hovered visuals differ.

A component theme can be overridden locally, so inspect context when one control looks inconsistent.

final appTheme = ThemeData(
  useMaterial3: true,
  colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF2563EB)),
  textTheme: const TextTheme(
    headlineMedium: TextStyle(fontSize: 30, fontWeight: FontWeight.w700),
    bodyLarge: TextStyle(fontSize: 16, height: 1.5),
  ),
  appBarTheme: const AppBarTheme(centerTitle: false),
  elevatedButtonTheme: ElevatedButtonThemeData(
    style: ElevatedButton.styleFrom(minimumSize: const Size(120, 48)),
  ),
  cardTheme: const CardThemeData(margin: EdgeInsets.all(12), elevation: 1),
  inputDecorationTheme: const InputDecorationTheme(border: OutlineInputBorder()),
  extensions: const [StatusColours(success: Colors.green, warning: Colors.orange)],
);

MaterialApp(theme: appTheme, home: const Scaffold(body: SizedBox.shrink()));

Read theme roles at the point of use

Theme.of(context).colorScheme and textTheme return the closest active theme, including nested overrides. Use Builder when context was created above the Theme widget whose values are needed.

Caching ThemeData globally prevents widgets from responding to a runtime theme switch.

Widget themedMessage(BuildContext context, String message) {
  final theme = Theme.of(context);
  return Card(
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Text(message, style: theme.textTheme.bodyLarge),
    ),
  );
}

My LinkedIn design-system note explains how ThemeExtension can keep product roles out of feature globals.

Connect on LinkedInSagnik Bhattacharya
The Complete Flutter Guide course thumbnail

The Complete Flutter Guide: Build Android, iOS and Web apps

Go from scratch to building industry-standard apps with Riverpod, Firebase, animations, REST APIs, and more.

Enrol now

Add domain tokens with ThemeExtension

A ThemeExtension can carry semantic values such as success, warning, chart palette, or spacing not covered by Material roles. Implement copyWith and lerp so tokens transition correctly between themes.

A plain static constants class cannot interpolate during theme changes or vary by brightness.

Treat the theme as a design-system API

Start with a semantic ColorScheme rather than a bag of colours named after their appearance. Roles such as primary, surface, error, and their contrasting foregrounds tell components why a colour exists and allow light and dark schemes to differ safely. Derive recurring text, shape, spacing, or brand properties through theme data and ThemeExtension instead of reaching for unrelated globals.

Component themes are the right place for application-wide defaults, while a local override should explain a genuine exception. Resolve interactive states—disabled, hovered, focused, pressed, selected—rather than testing only a resting button. Copying a component’s entire default style freezes assumptions that the framework could otherwise provide. Prefer setting the values the design system owns and leave unspecified behaviour to the surrounding theme.

Theme access belongs in build so inherited changes trigger a rebuild. Capturing context-derived colours in a long-lived object can leave a screen using the previous mode after a switch. When a feature needs a temporary subsection treatment, wrap that subtree in Theme and create a focused copy rather than mutating shared state. Extensions should implement interpolation so animated theme changes do not snap custom tokens.

A useful audit page renders every reusable component across enabled, disabled, focused, selected, error, and loading states in both brightness modes. Add increased text scaling, right-to-left layout, keyboard navigation, and contrast checks. Golden tests can catch accidental token drift, but semantic and interaction tests still need to prove labels and focus indicators. During migration, change one semantic layer at a time and search for hard-coded colours that bypass it. The goal is not merely visual consistency; it is a stable contract that lets future screens inherit accessible decisions without duplicating them.

Third-party widgets and platform views may not consume every Flutter theme token automatically. Wrap their adapters in the design-system layer and document which semantic values are translated into package-specific properties. Native system surfaces such as text selection, status bars, and dialogs also need an explicit audit. When deprecating a custom token, migrate all call sites before reassigning its meaning; otherwise old and new components interpret the same name differently. A small lint or review checklist that rejects hard-coded production colours can preserve the boundary after the initial theme work is complete.

Token names should describe purpose and ownership, such as editor selection or warning container, rather than one current hue. That vocabulary lets a brand refresh change values without making call sites dishonest. Document fallback behaviour for extensions so a missing local token fails visibly during development instead of quietly borrowing an unrelated colour. Designers and developers can then discuss the same role when reviewing a component.

Theme evolution needs ownership. Keep foundational roles in one package or directory, review changes through a component gallery, and record which product decision each custom extension represents. A feature may request a semantic token but should not redefine it locally to make one screenshot pass. When a token is replaced, deprecate the old name, migrate call sites, and remove it only after every supported theme supplies the successor.

Reusable components can narrow the public styling surface further. A product button may accept intent and size while reading colour, shape, typography, density, and states from the theme. That prevents callers from creating inaccessible combinations through five unrelated parameters. Test the component gallery under light, dark, high contrast, text scaling, keyboard focus, hover, and disabled states, then add golden coverage for intentional visual contracts. Package widgets that ignore theme roles should be wrapped once at the boundary. Consistent defaults become valuable when they reduce decisions in feature code, not when they merely centralise a larger pile of arbitrary values.

Common mistakes

  • Centralise visual decisions in ThemeData: In ThemeData design, hard-coded styling in feature screens slowly creates near-duplicate colours and radii; inspect this ThemeData design cause before changing another ThemeData design widget.
  • Generate a coherent ColorScheme from a seed: In ThemeData design, using primary as a background without its matching onPrimary can break contrast; inspect this ThemeData design cause before changing another ThemeData design widget.
  • Define an intentional textTheme: In ThemeData design, assigning arbitrary font sizes in each Text widget undermines scaling and hierarchy; inspect this ThemeData design cause before changing another ThemeData design widget.
  • Configure Material components once: In ThemeData design, a component theme can be overridden locally, so inspect context when one control looks inconsistent; inspect this ThemeData design cause before changing another ThemeData design widget.
  • Read theme roles at the point of use: In ThemeData design, caching ThemeData globally prevents widgets from responding to a runtime theme switch; inspect this ThemeData design cause before changing another ThemeData design widget.
  • Add domain tokens with ThemeExtension: In ThemeData design, a plain static constants class cannot interpolate during theme changes or vary by brightness; inspect this ThemeData design cause before changing another ThemeData design widget.

Frequently asked questions

How does centralise visual decisions in ThemeData work in ThemeData design?

For ThemeData design, the starting rule is that materialApp theme provides shared colour, typography, density, shape, and component defaults to descendants. Apply this ThemeData design rule first because centralise visual decisions in ThemeData determines whether the ThemeData design pattern fits.

Why does define an intentional textTheme matter for ThemeData design?

In ThemeData design, set font family, weight, height, and letter spacing at role level, then copyWith for exceptional emphasis. Keeping define an intentional textTheme at the ThemeData design call site exposes the ThemeData design return value directly.

What failure should I test first in ThemeData design?

First reproduce the ThemeData design case where caching ThemeData globally prevents widgets from responding to a runtime theme switch. The repaired widget reads semantic theme roles from its current build context and responds immediately when the active scheme changes.

How can I verify ThemeData design before release?

Exercise add domain tokens with ThemeExtension with real ThemeData design inputs on every shipped platform. Inspect the final ThemeData design callback or output; a successful ThemeData design button tap alone is not proof.

Further reads

Connect ThemeData design to the surrounding Flutter stack through these published tutorials:

Sources: Flutter framework and Dart API documentation; ThemeData design examples verified against current stable Flutter.