Build a Reusable Flutter Design System

Build a Reusable Flutter Design System

FlutterPulse

This article was translated specially for the channel FlutterPulseYou'll find lots of interesting things related to Flutter on this channel. Don't hesitate to subscribe!πŸš€

Make beautiful, consistent UIs that teams can actually ship and maintain.

You probably know the feeling: multiple designers, multiple devs, the app looks slightly different in different screens, and three months later, nobody's sure which PrimaryButton to use. A reusable design system fixes that. It's not just a pretty token file β€” it's a contract between design and engineering that scales with your product and team.

Below is a practical, senior-dev guide for building a reusable Flutter design system with real code patterns, folder layouts, testing tips, and governance advice so your UI stays consistent, accessible, and easy to evolve.

What a design system actually is

A design system = tokens + components + docs + rules.

  • Tokens: Colors, typography, spacing, elevation β€” the atomic values.
  • Components: Reusable widgets built from tokens (buttons, inputs, cards).
  • Docs: Examples, usage rules, API docs, migration guidance.
  • Rules & governance: What to change, release cadence, deprecations.

Treat tokens as the source of truth. Everything else should consume tokens so a color or spacing change ripples predictably.

1) Start with tokens (and keep them simple)

Create a token library in Dart. Keep tokens semantic (not "blue-500") β€” use names like primary, surface, danger, bodyLarge.

// packages/design_tokens/lib/tokens.dart
import 'package:flutter/material.dart';
class AppColors {
static const Color primary = Color(0xFF0066FF);
static const Color primaryVariant = Color(0xFF0047C7);
static const Color surface = Color(0xFFFFFFFF);
static const Color onSurface = Color(0xFF111827);
static const Color danger = Color(0xFFEF4444);
// Add semantic variables only - keep raw palette in a private place
}
class AppTypography {
static const TextStyle heading = TextStyle(
fontSize: 20,
fontWeight: FontWeight.w700,
letterSpacing: 0.15,
);
static const TextStyle body = TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
);
}
class AppSpacing {
static const double xs = 4;
static const double sm = 8;
static const double md = 16;
static const double lg = 24;
static const double xl = 40;
}

Tips:

  • Use semantic names so designers and devs reason the same way.
  • Store tokens in a separate package (e.g., design_tokens) so any app or package can depend on them.

2) Build atomic, accessible components

Components are thin wrappers around primitives that enforce tokens and accessibility rules.

Example: a primary button.

// packages/design_components/lib/buttons/primary_button.dart
import 'package:flutter/material.dart';
import 'package:design_tokens/tokens.dart';

class PrimaryButton extends StatelessWidget {
final VoidCallback onPressed;
final Widget child;
final bool filled;
const PrimaryButton({
required this.onPressed,
required this.child,
this.filled = true,
super.key,
});
@override
Widget build(BuildContext context) {
final bg = filled ? AppColors.primary : Colors.transparent;
final fg = filled ? Colors.white : AppColors.primary;
return ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: bg,
foregroundColor: fg,
padding: const EdgeInsets.symmetric(
vertical: AppSpacing.sm,
horizontal: AppSpacing.md,
),
textStyle: AppTypography.body,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
onPressed: onPressed,
child: child,
);
}
}

Accessibility rules baked in:

  • Ensure minimum touch target (44–48 px).
  • Contrast check: prefer tokens that pass WCAG contrast (β‰₯4.5:1 for text).
  • Keyboard focus and semantics for all interactive widgets.

3) Theming & runtime configuration

Expose a ThemeData factory that uses tokens. Allow runtime overrides (brand color or density).

// packages/design_theme/lib/theme.dart
import 'package:flutter/material.dart';
import 'package:design_tokens/tokens.dart';

ThemeData buildAppTheme({bool dark = false, Color? accent}) {
final primary = accent ?? AppColors.primary;
return ThemeData(
colorScheme: ColorScheme(
brightness: dark ? Brightness.dark : Brightness.light,
primary: primary,
onPrimary: Colors.white,
surface: AppColors.surface,
onSurface: AppColors.onSurface,
secondary: primary,
onSecondary: Colors.white,
background: dark ? Color(0xFF0B1220) : Colors.white,
onBackground: AppColors.onSurface,
error: AppColors.danger,
onError: Colors.white,
brightness: dark ? Brightness.dark : Brightness.light,
),
textTheme: TextTheme(
headline6: AppTypography.heading,
bodyText2: AppTypography.body,
),
useMaterial3: true,
);
}

Expose a DesignSystem widget (or provider) that apps wrap with to make theming and tokens available.

4) Organize as packages (monorepo recommended)

A monorepo with multiple packages scales best:

/packages/
/design_tokens/
/design_components/
/design_theme/
/design_docs/ (UI catalog / examples)
apps/
/mobile_app/

Use melos (or similar) for workspace management and incremental changes.

5) Document everything β€” living examples are the heart

Docs are not a PDF. Use a live component catalog (Storybook, Dashbook, or a small Flutter docs app) that shows:

  • All variants
  • Usage patterns
  • Dos & don'ts
  • Accessibility notes
  • Code snippets and migration examples

A good example acts as a test, adoption documentation, and onboarding material.

6) Versioning & release strategy

Treat the design system like a product:

  • Use semantic versioning: major bumps for breaking API changes.
  • Keep a CHANGELOG and migration guide for each release.
  • Mark deprecated APIs @Deprecated(...) and keep them for one minor version before removal.
  • Consider nightly or canary channels for internal teams to test upcoming changes.

7) Tests & CI: golden tests, unit tests, and contract checks

  • Golden tests for widget visuals (buttons, inputs, cards) ensure no visual regressions.
  • Unit tests to verify token values and component behavior.
  • Contract tests: if a component exposes a public API, have a small test suite that consumers can run to ensure compatibility.

CI should run relevant package tests and golden comparisons. For golden images, use stable device pixel ratios and deterministic fonts.

8) Accessibility, internationalization & RTL

  • Validate color contrast automatically (CI step).
  • Support RTL by testing layouts with Directionality(textDirection: TextDirection.rtl, ...).
  • Avoid hard-coded strings in components; allow label and semanticLabel params and demonstrate i18n patterns in docs.

9) Governance & adoption

  • Appoint owners/maintainers for tokens, components, and docs.
  • Define a review process for new components β€” a short design + API proposal before adding.
  • Enforce usage by integrating style checks into code review templates and CI.

Quick starter checklist

  • Create a design_tokens package (semantic tokens only).
  • Build design_components that consume tokens exclusively.
  • Expose design_theme for runtime customization.
  • Add a live design_docs app with usage examples and snippets.
  • Add golden & unit tests for every component.
  • Automate CI to test changed packages and run golden checks.
  • Publish internally or to pub.dev with semantic versioning and CHANGELOG.
  • Document deprecation policy & migration guides.

Final note β€” ship small, iterate fast

Start with the 10% of components your product uses most (buttons, text fields, cards, typography, spacing). Make those rock-solid, documented, and tested. If the team feels the benefit early, adoption becomes natural β€” and you can expand confidently.

Read more like this:

Flutter β€” Writing Cleaner Code (Beyond UI)

Make your code easier to read, test, and maintain

medium.com

Flutter: Remove all unused imports instantly

One command that will ruleβ€” (quick, safe, and repeatable)

medium.com

9 Silent Mistakes Killing Your Flutter App

Mistakes I Made in Flutter & How to Avoid Them?

medium.com

Report Page