All articles
6 min read
October 23, 20266 min read

Building Multi-Brand Design Systems in Flutter with ThemeExtensions & Tokens

Design and architect a multi-brand, cross-platform design system in Flutter using design tokens, standalone UI packages, dynamic color schemes, and widget previews.

Tags:#Flutter#Design System#ThemeExtension#UI/UX#Mobile
Building Multi-Brand Design Systems in Flutter with ThemeExtensions & Tokens

As engineering organizations scale, multiple mobile products often share common foundations: design language, typography scales, interaction models, and accessibility baselines.

Hardcoding colors like Color(0xFF2563EB) across UI components results in maintenance bottlenecks when re-branding or adding Dark Mode. In this article, we implement a token-based design system in Flutter utilizing ThemeExtension.

Design Systems and Tokens
Design Systems and Tokens

Design tokens decouple visual styling from individual UI widget implementations.

What are Design Tokens?

Design tokens are design atomics: named variables representing colors, spacing increments, radii, and typography styles:

  1. Global Tokens: Raw values (blue-500: #3B82F6, radius-md: 12px).
  2. Semantic Tokens: Purpose-driven mappings (surface-background, action-primary, feedback-error).
  3. Component Tokens: Widget-specific bindings (button-primary-background, badge-border).

Creating a Semantic ThemeExtension

Flutter's built-in ThemeData covers basic Material properties, but lacks custom brand accents. ThemeExtension bridges this gap with seamless interpolation:

dart
import 'package:flutter/material.dart';

@immutable
class AppBrandTokens extends ThemeExtension<AppBrandTokens> {
  final Color accentGradientStart;
  final Color accentGradientEnd;
  final Color surfaceSubtle;
  final double radiusCard;

  const AppBrandTokens({
    required this.accentGradientStart,
    required this.accentGradientEnd,
    required this.surfaceSubtle,
    required this.radiusCard,
  });

  @override
  AppBrandTokens copyWith({
    Color? accentGradientStart,
    Color? accentGradientEnd,
    Color? surfaceSubtle,
    double? radiusCard,
  }) {
    return AppBrandTokens(
      accentGradientStart: accentGradientStart ?? this.accentGradientStart,
      accentGradientEnd: accentGradientEnd ?? this.accentGradientEnd,
      surfaceSubtle: surfaceSubtle ?? this.surfaceSubtle,
      radiusCard: radiusCard ?? this.radiusCard,
    );
  }

  @override
  AppBrandTokens lerp(ThemeExtension<AppBrandTokens>? other, double t) {
    if (other is! AppBrandTokens) return this;
    return AppBrandTokens(
      accentGradientStart: Color.lerp(accentGradientStart, other.accentGradientStart, t)!,
      accentGradientEnd: Color.lerp(accentGradientEnd, other.accentGradientEnd, t)!,
      surfaceSubtle: Color.lerp(surfaceSubtle, other.surfaceSubtle, t)!,
      radiusCard: radiusCard + (other.radiusCard - radiusCard) * t,
    );
  }
}

Registering and Consuming Tokens

dart
// In your App Theme configuration:
final lightTheme = ThemeData.light().copyWith(
  extensions: [
    const AppBrandTokens(
      accentGradientStart: Color(0xFF6366F1),
      accentGradientEnd: Color(0xFF8B5CF6),
      surfaceSubtle: Color(0xFFF9FAFB),
      radiusCard: 16.0,
    ),
  ],
);

// In any widget:
class BrandedCard extends StatelessWidget {
  const BrandedCard({super.key});

  @override
  Widget build(BuildContext context) {
    final tokens = Theme.of(context).extension<AppBrandTokens>()!;

    return Container(
      decoration: BoxDecoration(
        color: tokens.surfaceSubtle,
        borderRadius: BorderRadius.circular(tokens.radiusCard),
        border: Border.all(color: Colors.black12),
      ),
      padding: const EdgeInsets.all(20),
      child: Text('Token-Driven Design Component', style: Theme.of(context).textTheme.titleMedium),
    );
  }
}

Architecture Takeaways

  • Centralize color palettes into semantic tokens.
  • Implement lerp inside ThemeExtension to guarantee smooth transitions during theme switches.
  • Test isolated widgets using Flutter 3.47+ Widget Previews without booting the full app.