skip to content

Themes & Color Schemes

ThemeData built from ColorScheme.fromSeed, a TextTheme and component themes, extended with ThemeExtension tokens and switched by themeMode. Interviewers ask how you theme without hardcoded colors.

part ofFlutteroverview, primer and where to startread it →
on this pageshow

explore

questions

5

In Flutter, how do you style widgets from the app theme instead of hardcoding colors and text styles?

level: juniorimportance: must knowfreq 68%

answer

  1. one ThemeData at the app root
  2. MaterialApp's theme parameter
  3. Theme.of(context) walks up the tree
  4. colorScheme roles like primary and onPrimary
  5. textTheme styles like bodyMedium

basics

~10 s

Define one ThemeData, usually with a ColorScheme and a TextTheme, pass it to MaterialApp's theme, then read Theme.of(context).colorScheme and .textTheme in widgets instead of writing Colors.blue or a literal TextStyle.

solid answer

~40 s

I put a single `ThemeData` on `MaterialApp.theme`, built from a `ColorScheme` (for example `ColorScheme.fromSeed`) and a `TextTheme`. Material widgets such as `AppBar`, `FilledButton` and `Card` already read their defaults from it. In my own widgets I read `Theme.of(context).colorScheme.primary` or `Theme.of(context).textTheme.titleLarge` rather than `Colors.indigo` or a literal `TextStyle(fontSize: 22)`. I pair every background role with its `on*` role, so text on `primary` uses `onPrimary`. When a style needs a tweak I use `copyWith` on the theme style, so the brand change or dark theme still flows through. The payoff is one place to change the brand and a dark theme that works without touching screens.

code

dart · 31 lines
dart
import 'package:flutter/material.dart';

void main() => runApp(const LoyaltyApp());

class LoyaltyApp extends StatelessWidget {
  const LoyaltyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF00696B)),
      ),
      home: Builder(
        builder: (BuildContext context) {
          final ColorScheme scheme = Theme.of(context).colorScheme;
          return Scaffold(
            body: Center(
              child: Text(
                'Welcome back',
                style: Theme.of(context).textTheme.headlineSmall?.copyWith(
                  color: scheme.primary,
                ),
              ),
            ),
          );
        },
      ),
    );
  }
}

go deeper

for a junior

Recall the chain: ThemeData on MaterialApp.theme, then Theme.of(context).colorScheme and .textTheme in build. Name a few roles such as primary, onPrimary, surface and bodyMedium.

for a middle

Explain why on-roles exist, why copyWith on a theme style beats a new TextStyle, and why Theme.of must be called from a context below MaterialApp.

for a senior

Show how you keep a codebase free of literal colors: lint rules or review checks, a theme file per app, and clear exceptions for brand assets.

for a principal

Frame theming as a contract between design and code: roles as the shared vocabulary, so redesigns and new modes land by editing ThemeData rather than screens.

## What a theme is in Flutter A **theme** in Flutter's Material library is a `ThemeData` object: an immutable bundle of colors, typography, shapes and per-component defaults. You hand it to `MaterialApp` through the `theme` parameter, and `MaterialApp` places it in the widget tree so that every descendant can look it up. If you pass nothing, `MaterialApp` builds a default `ThemeData()` for you, which in current Flutter is a Material 3 theme (`useMaterial3` has defaulted to `true` since Flutter 3.16). The two properties that almost every app sets are: - **`colorScheme`** — a `ColorScheme` holding the Material 3 color roles: `primary`, `onPrimary`, `primaryContainer`, `secondary`, `tertiary`, `error`, `surface`, `onSurface`, the `surfaceContainer*` family, `outline` and more. - **`textTheme`** — a `TextTheme` holding fifteen named styles in five groups: `display`, `headline`, `title`, `body` and `label`, each in `Large`, `Medium` and `Small` (for example `bodyMedium`, `titleLarge`, `labelSmall`). ## Reading the theme in a widget Inside `build`, call `Theme.of(context)`. It returns the `ThemeData` of the nearest `Theme` ancestor, which is normally the one `MaterialApp` installed. From there you read roles, not raw values: ```dart import 'package:flutter/material.dart'; class PointsBanner extends StatelessWidget { const PointsBanner({super.key, required this.points}); final int points; @override Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); return ColoredBox( color: theme.colorScheme.primaryContainer, child: Padding( padding: const EdgeInsets.all(16), child: Text( '$points points', style: theme.textTheme.titleLarge?.copyWith( color: theme.colorScheme.onPrimaryContainer, ), ), ), ); } } ``` Two conventions carry most of the value: 1. **Pair every surface role with its `on*` role.** Text or icons drawn on `primary` use `onPrimary`; content on `surface` uses `onSurface`. The scheme is generated so that these pairs are legible in both light and dark. 2. **Adjust a theme style with `copyWith`, never replace it.** `textTheme.titleLarge?.copyWith(color: ...)` keeps the size, weight and font the theme chose; a fresh `TextStyle(fontSize: 22)` silently opts out of any later typography change. The `TextTheme` fields are nullable, hence the `?.`. ## What the Material widgets already do You rarely need to pass colors to Material components at all. `AppBar`, `FilledButton`, `NavigationBar`, `Card`, `Checkbox` and the rest compute their defaults from `colorScheme` and `textTheme`. That means a theme change restyles them automatically, and your own widgets stay consistent with them only if they also read the theme. | Approach | Brand change | Dark theme | Consistency with Material widgets | |---|---|---|---| | `Colors.indigo`, literal `TextStyle` | Edit every file | Breaks or needs `if` checks | Drifts | | `Theme.of(context).colorScheme` / `.textTheme` | Edit the `ThemeData` only | Follows the active theme | Matches | ## Alpha and the `Color` API in current Flutter When a design asks for a translucent version of a role color, use **`Color.withValues(alpha: 0.12)`**. Since Flutter 3.27 a `Color` stores its components as floating-point values, and `withOpacity` and `.opacity` are deprecated in favour of `withValues` and `.a`, because the old methods quantised alpha to 8 bits. The old methods still exist but produce deprecation warnings. ## Common mistakes - Hardcoding `Colors.white` for text on a colored surface, which becomes invisible in a light or dark variant. - Creating a new `ThemeData()` inside a screen to get "a theme", which throws away every app-level setting instead of inheriting it. - Assuming a `TextTheme` style is non-null and calling methods on it without `?.` or a fallback. - Styling a custom widget from `Colors.grey` because "it is only a divider", which then ignores `colorScheme.outlineVariant` and looks wrong in dark mode. ## Theme.of versus the shortcuts `Theme.of(context)` is the general entry point. The Material library also offers shortcuts that read one part of it: `ColorScheme.of(context)` returns `Theme.of(context).colorScheme`, and `TextTheme.of(context)` returns `Theme.of(context).textTheme`. They behave identically; use whichever reads better, but be consistent within a codebase. What you should not do is cache a `ThemeData` in a field or a global variable: the active theme changes when the user switches to dark mode or when a subtree wraps itself in a local `Theme`, and only a lookup through the current `context` sees the right one. ## Where this leads Once widgets read roles from `Theme.of(context)`, you can add a `darkTheme`, generate schemes from a seed color, add brand tokens through `ThemeExtension`, and set per-component defaults through component themes such as `FilledButtonThemeData`, all without editing screens. That is the whole reason interviewers ask this question: they want to hear that styling decisions live in one `ThemeData`, and widgets ask for meaning (primary, error, title) rather than for a specific hex value.

  • In Flutter, why does Theme.of(context) inside MaterialApp's own build method not see the theme you just passed to it?
    `Theme.of` looks for the nearest `Theme` ancestor of the given context. The context of the widget that builds `MaterialApp` sits above the `Theme` that `MaterialApp` inserts, so the lookup finds nothing app-specific and returns the fallback theme. Read the theme from a descendant context: a child widget's `build`, or a `Builder` placed under `MaterialApp`.
  • In Flutter 3.27 and later, what replaces Color.withOpacity, and why was it deprecated?
    `Color.withValues(alpha: 0.5)` replaces `withOpacity(0.5)`, and `.a` replaces `.opacity`. Since 3.27 `Color` stores components as floats to support wide-gamut color, so the old 8-bit opacity methods lose precision. They were deprecated rather than removed, and still work with a warning.
  • In Flutter's Material library, when is it acceptable to write a literal color in a widget?
    When the color is genuinely not part of the design language: a third-party brand logo's fixed color, a chart palette that the design keeps separate, or a debug overlay. Even then, a named constant or a `ThemeExtension` field beats an inline literal, because a reviewer can see it was a deliberate exception.

saying these in an interview costs you the question

  • Writing Colors.white for text because the button background is dark
  • Creating a fresh ThemeData() in a screen to style one widget
  • Replacing a TextTheme style with a new TextStyle instead of copyWith
  • Using withOpacity as the current way to make a role color translucent
  • Believing Material widgets ignore ThemeData unless every color is passed explicitly
open as a page

In Flutter, how do ColorScheme.fromSeed, darkTheme and themeMode work together to give an app matching light and dark themes?

level: middleimportance: must knowfreq 55%

basics

~20 s

ColorScheme.fromSeed derives every Material 3 color role from one seed color for a given brightness. Build a light and a dark ThemeData from the same seed, pass them as theme and darkTheme, and themeMode (default ThemeMode.system) picks one.

open as a page

In Flutter, how do you add custom design tokens to ThemeData with ThemeExtension, and why must you implement copyWith and lerp?

level: middleimportance: should knowfreq 42%

basics

~10 s

Subclass ThemeExtension<T> with your token fields, implement copyWith and lerp, register instances in ThemeData.extensions for each theme, and read them with Theme.of(context).extension<T>(). lerp lets the tokens animate when the theme changes.

open as a page

In a Flutter app, one screen's FilledButton ignores the app's filledButtonTheme; what style resolution order explains it, and how do local Theme overrides fit in?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Material buttons resolve each property from the widget's own style first, then the nearest component theme (a FilledButtonTheme widget, else ThemeData.filledButtonTheme), then built-in defaults from the ColorScheme. A local style, a nearer theme or a fresh ThemeData explains the mismatch.

open as a page

In a white-label Flutter loyalty app rebranded per retailer with dark mode, how would you structure ThemeData so no screen hardcodes a brand value?

level: seniorimportance: should knowfreq 32%

basics

~20 s

Model each retailer as a small brand config (seed, optional exact colors, fonts, token values), and build light and dark ThemeData from it in one factory: fromSeed schemes, a TextTheme, component themes and ThemeExtension tokens. Screens only read Theme.of(context).

open as a page