Theming & Design Systems
Irbisa · cheatsheetSeptember 13, 2026

Theming & Design Systems

ThemeData, ColorScheme.fromSeed, Material 3, ThemeExtension, custom fonts, dark mode

Middle Developer16 itemscompressed for a skim
  1. 01

    Your seed colour is ignored and the app renders in baseline purple, but only in the widget that builds MaterialApp — why?

    Easy

    Theme.of(context) walks up from the context you hand it to the nearest Theme inherited widget, and MaterialApp installs yours below itself — so a context taken above MaterialApp finds nothing and gets the fallback theme.

    const brand = Color(0xFF00695C);
    
    // ❌ this context is ABOVE the MaterialApp the same method builds
    class App extends StatelessWidget {
      const App({super.key});
    …
  2. 02

    Design hands you one brand hex, but ColorScheme.fromSeed returns a primary that is not that colour — what happened, and which roles do you build with?

    Easy

    fromSeed does not use your hex as primary; it treats it as a seed, derives tonal palettes from it in the HCT colour space, and reads each role off a fixed tone so every colour and its on partner are legible by construction.

    const brand = Color(0xFF00695C);
    
    final scheme = ColorScheme.fromSeed(
      seedColor: brand,                    // a seed, not the resulting primary
      brightness: Brightness.light,
    );
    …
  3. 03

    Your screens build every heading with a hand-written TextStyle — what do the Material 3 TextTheme roles give you that this throws away?

    Easy

    A role is a name your design system can redefine in one place; a literal TextStyle is a copy of that decision now living in forty files.

    // ❌ one decision, copied into every screen
    Text('Monthly total',
        style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w600));
    
    // ✅ a role the theme owns
    Text('Monthly total', style: Theme.of(context).textTheme.titleLarge);
    …
  4. 04

    Your app-wide filledButtonTheme is set, but a screen that passes its own style now shows brand-coloured disabled buttons — how does a component style resolve?

    Medium

    A Material component merges three styles property by property — the call site's style, then the component theme from ThemeData, then the widget's built-in defaults — and for each property the first non-null value wins.

    const brand = Color(0xFF00695C);
    
    final theme = ThemeData(
      colorScheme: ColorScheme.fromSeed(seedColor: brand),
      filledButtonTheme: FilledButtonThemeData(
        style: FilledButton.styleFrom(
    …
  5. 05

    Where do brand tokens with no ColorScheme slot — a success green, a hero gradient, a grid gutter — live so they still work when the theme changes?

    Medium

    In a ThemeExtension<T> registered on ThemeData.extensions, which makes them reachable through Theme.of(context) exactly like the built-in roles and lets them interpolate while the theme animates.

    @immutable
    class BrandTokens extends ThemeExtension<BrandTokens> {
      const BrandTokens({
        required this.success,
        required this.heroGradient,
        required this.gutter,
    …
  6. 06

    Your brand purple is unreadable on dark surfaces and the designer's dark theme is not the light one inverted — how do you build the two ThemeData objects?

    Medium

    Write one function that takes a Brightness and returns a complete ThemeData, and let ColorScheme.fromSeed choose different tones for each — the dark theme's primary is a lighter tone of the same hue precisely because the mid tone is unreadable on a dark surface.

    const _brand = Color(0xFF6750A4);
    
    ThemeData buildTheme(Brightness brightness) {
      final cs = ColorScheme.fromSeed(seedColor: _brand, brightness: brightness);
      return ThemeData(
        colorScheme: cs,                       // brightness comes from here
    …
  7. 07

    Your headings ask for FontWeight.w700 but render at normal weight on the device — what is wrong with the font setup?

    Medium

    Flutter matches a requested weight against the faces you declared in pubspec.yaml and falls back to the nearest one, so if the only Inter file you shipped is Regular, every w700 in the app renders at 400.

    # pubspec.yaml — every weight the design uses is a separate declared file
    flutter:
      uses-material-design: true
    
      fonts:
        - family: Inter
    …
  8. 08

    You are dropping useMaterial3: false from an app that never migrated — what visibly changes, and what regresses without anyone noticing?

    Medium

    Material 3 swaps a primaryColor-driven look for a ColorScheme-driven one: components take colour from scheme roles, layers separate by surface tone instead of shadow, and the type scale and corner radii change underneath every screen.

    // ── Before: Material 2 wiring ──
    ThemeData(
      useMaterial3: false,                     // deprecated escape hatch
      primaryColor: const Color(0xFF3F51B5),   // drove AppBar, FAB and buttons
      appBarTheme: const AppBarTheme(elevation: 4),
    );
    …
  9. 09

    CupertinoSwitch and CupertinoButton inside your MaterialApp ignore the brand colour and the custom font — what does MaterialApp actually hand CupertinoTheme?

    Medium

    MaterialApp exposes your ThemeData to Cupertino widgets through MaterialBasedCupertinoThemeData, which bridges four values and leaves everything else at the iOS defaults.

    final theme = ThemeData(
      colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF0B6E4F)),
      fontFamily: 'Inter',
    
      // Only brightness, primaryColor, primaryContrastingColor and
      // scaffoldBackgroundColor are derived from the Material theme above.
    …
  10. 10

    Product wants Material You colours from the Android wallpaper, marketing wants the brand purple — how do you ship both, and when do you refuse dynamic colour outright?

    Medium

    Flutter has no built-in wallpaper palette: you read it with the dynamic_color package and fall back to ColorScheme.fromSeed(brandSeed) on every platform and OS version that has none.

    import 'package:dynamic_color/dynamic_color.dart';
    import 'package:flutter/material.dart';
    
    const _brandSeed = Color(0xFF6750A4);
    
    class App extends StatelessWidget {
    …
  11. 11

    The same FilledButton is visibly shorter in your macOS build than on the phone, with no platform code anywhere. What is doing that, and how do you take control?

    Medium

    ThemeData defaults visualDensity to VisualDensity.defaultDensityForPlatform(platform), which is standard on Android, iOS and Fuchsia and compact on macOS, Windows and Linux.

    // The default nobody writes down:
    //   android / ios / fuchsia   -> VisualDensity.standard  ( 0,  0)
    //   macos / windows / linux   -> VisualDensity.compact   (-2, -2)  = 8px smaller
    final inherited = ThemeData(
      colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF0B6E4F)),
    );
    …
  12. 12

    Three apps share one design system. What does that package actually export, and how do you change a token without breaking a release you don't control?

    Medium

    It exports semantic tokens, a ThemeData factory and the components — never raw constants — and every change to that surface is versioned like any other public API.

    // packages/acme_design/lib/acme_design.dart — the entire public surface
    export 'src/theme/acme_theme.dart' show AcmeTheme;
    export 'src/theme/acme_tokens.dart' show AcmeTokens, AcmeSpacing, AcmeThemeX;
    export 'src/components/acme_empty_state.dart' show AcmeEmptyState;
    // src/** stays internal: apps cannot import it, so it is free to change.
    …
  13. 13

    When should a design system ship its own InheritedWidget instead of putting its tokens in a ThemeExtension, and what do you give up either way?

    Hard

    Use a ThemeExtension when the value is theme-shaped and should interpolate with the theme, and your own InheritedWidget when it is not — a scope with a different lifetime, a different change rate, or values ThemeData has no business carrying.

    import 'dart:ui' show lerpDouble;
    import 'package:flutter/material.dart';
    
    // ── Theme-shaped and lerpable: ThemeExtension ──
    @immutable
    class AcmeTokens extends ThemeExtension<AcmeTokens> {
    …
  14. 14

    Switching to dark mode flashes white for a frame and your brand gradient jumps instead of fading. Where does each of those come from?

    Hard

    MaterialApp already wraps the app in an AnimatedTheme that lerps ThemeData over kThemeAnimationDuration (200 ms), so anything that jumps is either outside that widget or a value ThemeData.lerp refuses to interpolate.

    MaterialApp(
      theme: AcmeTheme.light(),
      darkTheme: AcmeTheme.dark(),
      themeMode: mode,
      // MaterialApp inserts AnimatedTheme for you; these are its knobs.
      themeAnimationDuration: const Duration(seconds: 1), // slow it down to debug
    …
  15. 15

    How do you stop a pull request from landing a raw Color(0xFF...) or an EdgeInsets.all(13) once the design system exists?

    Hard

    You make it a build failure instead of a review comment: a custom lint that flags the raw literal, a checked-in baseline so the existing count can only fall, and per-theme goldens for the mistakes a lint cannot see.

    // packages/acme_lints/lib/acme_lints.dart
    import 'package:analyzer/error/listener.dart';
    import 'package:custom_lint_builder/custom_lint_builder.dart';
    
    PluginBase createPlugin() => _AcmeLints();
    …
  16. 16

    You inherit an app with 400 hard-coded colours and text styles, and a release freeze is not an option. How do you get it onto the design system?

    Hard

    You strangle it feature by feature behind a compatibility layer: define the tokens first, point every legacy constant at one, then delete the constants a screen at a time while CI ratchets the remaining count down.

    // 1 — inventory, before writing any code
    //   grep -rEn "Color\(0x|Colors\.[a-z]" lib | wc -l              -> 412
    //   grep -rEoh "Color\(0x[0-9A-Fa-f]{8}\)" lib | sort | uniq -c  -> 31 distinct
    //        71 Color(0xFFF5F5F5)   58 Color(0xFF2E7D32)   44 Color(0xFFF6F6F6)
    //   (0xFFF5F5F5 and 0xFFF6F6F6 are the same design decision, typed twice.)
    …