skip to content

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%

answer

  1. three layers: widget, theme, defaults
  2. style: argument wins per property
  3. nearest FilledButtonTheme before ThemeData
  4. Theme(data: Theme.of(context).copyWith(...))
  5. fresh ThemeData drops inherited settings

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.

solid answer

~40 s

For `FilledButton`, each property is resolved per state as `widget.style` first, then the component theme style, then `defaultStyleOf`, which derives from `colorScheme`. The component theme comes from the nearest `FilledButtonTheme` widget, falling back to `Theme.of(context).filledButtonTheme`. So on the odd screen I look for three things: a `style:` or `FilledButton.styleFrom` on the button, a `FilledButtonTheme` or `Theme` wrapper somewhere above it, or a local `Theme(data: ThemeData(...))` that built a fresh theme instead of `Theme.of(context).copyWith(...)`, which silently discards the app's component themes. The fix is usually to move the one-off style into the theme or to extend the parent theme with `copyWith`. Flutter DevTools' widget inspector shows the ancestors, which makes the wrapper easy to find.

go deeper

for a junior

Remember the order: the button's own style, then the button's theme, then Material defaults from the color scheme.

for a middle

Explain per-property resolution, the nearest FilledButtonTheme lookup, and the difference between copyWith and a fresh ThemeData in a local Theme.

for a senior

Walk through diagnosing the inconsistent screen with the inspector, and set team rules that keep styling in component themes rather than per-widget styles.

for a principal

Weigh how much to encode in component themes versus wrapper widgets of your own, and how that choice affects future Material version upgrades.

## Three layers of button styling Flutter's Material buttons (`FilledButton`, `ElevatedButton`, `OutlinedButton`, `TextButton`) share a base class, `ButtonStyleButton`. Each subclass supplies two hooks, `themeStyleOf(context)` and `defaultStyleOf(context)`, and the base class resolves **every property separately** in this order: 1. **`widget.style`** — the `ButtonStyle` passed to that button, often built with `FilledButton.styleFrom(...)`. 2. **The component theme style** — for `FilledButton`, `FilledButtonTheme.of(context).style`. 3. **The defaults** — `defaultStyleOf(context)`, computed from the current `colorScheme` and `textTheme`. Because properties are resolved one by one, a button with `style: FilledButton.styleFrom(backgroundColor: ...)` still takes its shape and padding from the theme; only the properties it set are overridden. Most properties are `WidgetStateProperty` values, resolved against the button's current states such as pressed, hovered or disabled. (`WidgetStateProperty` replaced the older `MaterialStateProperty` name.) ## Where the component theme comes from `FilledButtonTheme.of(context)` looks for the **nearest `FilledButtonTheme` widget** above the button. Only if there is none does it fall back to **`Theme.of(context).filledButtonTheme`**, the `FilledButtonThemeData` in the active `ThemeData`. Every Material component follows the same idea: a `ThemeData` field (`cardTheme`, `appBarTheme`, `inputDecorationTheme`, `navigationBarTheme` and so on) plus, for many, an inherited widget that can override it for a subtree. Recent releases normalised the class names so that each `ThemeData` field holds an `*ThemeData` class: `CardThemeData`, `DialogThemeData` and `TabBarThemeData` in Flutter 3.27, `AppBarThemeData`, `BottomAppBarThemeData` and `InputDecorationThemeData` in 3.35. ## Local Theme overrides To restyle a subtree, wrap it in a `Theme` widget. There are two ways to build its `data`: | Local override | What the subtree gets | |---|---| | `Theme.of(context).copyWith(colorScheme: ...)` | Parent theme with one change; component themes, text theme and extensions kept | | `ThemeData(colorScheme: ...)` | A brand-new default theme; everything the app configured is lost | The second form is the classic cause of "this screen ignores our theme": the app's `filledButtonTheme`, `cardTheme`, custom `textTheme` and every `ThemeExtension` vanish below that point, and the buttons fall back to layer 3 defaults built from the new scheme. ## Diagnosing the odd screen When one screen's `FilledButton` looks wrong, work through the layers: - **Check the button itself.** A `style:` argument or `FilledButton.styleFrom` overrides the theme for the properties it sets. - **Check its ancestors.** A `FilledButtonTheme` wrapper, or a `Theme` wrapper, may sit in the screen or in a shared scaffold widget. The widget inspector in Flutter DevTools shows the ancestor chain. - **Check how the local `Theme` was built.** A fresh `ThemeData(...)` instead of `copyWith` drops the component theme entirely. - **Check the widget type.** An `ElevatedButton` is styled by `elevatedButtonTheme`, not `filledButtonTheme`; `FilledButton.tonal` uses the same `FilledButtonTheme` but different defaults. - **Check the state.** A disabled button (null `onPressed`) resolves the disabled entries of each `WidgetStateProperty`, which the theme may not have set. ## Designing so this does not recur - Put brand decisions in `ThemeData` component themes, not in per-button `styleFrom` calls, so there is one place to look. - When a subtree genuinely needs a variation, extend the parent with `Theme.of(context).copyWith(...)` or wrap only that subtree in a `FilledButtonTheme`. - Reserve `style:` on a single button for truly local exceptions, and review them. - Widget tests that pump a screen under the real app theme catch a stray fresh `ThemeData` early. ## Why per-property resolution is a feature Per-property resolution lets a design system set shape, padding, text style and colors once, while a genuinely exceptional button overrides a single property without restating the rest. It also means the order of layers is predictable across all Material buttons, so the same diagnosis works for `OutlinedButton` and `TextButton` with their own component themes. The cost is that a stray override is silent: nothing warns you that a `style:` on one button hides a later theme change, so it only shows up when someone compares screens. ## A worked example ```dart import 'package:flutter/material.dart'; Widget checkoutSection(BuildContext context) { final ThemeData parent = Theme.of(context); return Theme( // Keeps the app's filledButtonTheme, cardTheme and extensions. data: parent.copyWith( colorScheme: parent.colorScheme.copyWith(primary: parent.colorScheme.tertiary), ), child: FilledButton(onPressed: () {}, child: const Text('Redeem')), ); } ``` Here the button keeps the app's shape and padding from `filledButtonTheme` and picks up the swapped `primary` through the defaults layer, because the component theme did not set a background color. Had the component theme set `backgroundColor`, it would still win over the defaults, and the swap would have no visible effect, which is another frequent surprise.

  • In Flutter, if FilledButtonThemeData sets backgroundColor, does changing colorScheme.primary in a local Theme recolor the button?
    No. The component theme sits above the defaults in resolution order, so its `backgroundColor` wins, and `primary` only feeds the defaults layer. Either change the component theme in the local override too, or leave `backgroundColor` out of the component theme so it keeps following `primary`.
  • In Flutter, when would you use a FilledButtonTheme widget instead of a Theme wrapper?
    When only filled buttons in a subtree should change. `FilledButtonTheme(data: ..., child: ...)` overrides just that component's theme, is cheaper to reason about, and leaves colors, text styles and extensions for everything else untouched.

saying these in an interview costs you the question

  • Believing ThemeData.filledButtonTheme always wins over a style argument
  • Wrapping a subtree in Theme(data: ThemeData(...)) to change one color
  • Expecting filledButtonTheme to style ElevatedButton as well
  • Assuming a widget's style replaces the whole theme style rather than per property
  • Still writing MaterialStateProperty in new code instead of WidgetStateProperty