Skip to content

fabrik_theme

pub.dev

Flutter’s ThemeData gives you colorScheme.primary and textTheme.bodyLarge. That covers Material’s own widgets, but says nothing about your app’s roles — the color of a helper label, the style of an emphasised value in a list row.

fabrik_theme adds a semantic layer on top: you define colors and text styles once, by meaning, and read them anywhere through context.

Text('Payment failed', style: TextStyle(color: context.colors.error))
Text('Last updated', style: context.typography.bodySmallSecondary)
dependencies:
fabrik_theme: ^1.1.0
import 'package:fabrik_theme/fabrik_theme.dart';

Build both themes from the bundled palettes and you have a working light/dark app in one step:

MaterialApp(
theme: FabrikTheme.create(
brightness: Brightness.light,
colors: AppColors.defaults(),
),
darkTheme: FabrikTheme.create(
brightness: Brightness.dark,
colors: AppColors.darkDefaults(),
),
home: const HomePage(),
);

Then read colors and typography anywhere below:

Container(
color: context.colors.surface,
child: Text('Welcome', style: context.typography.headlineMedium),
);

Typography is generated from your colors automatically — you only supply an AppTypography if you want to override it.

AppColors is a contract of semantic roles. Define one palette per brightness:

class AppPalette {
static const light = AppColors(
primary: Color(0xFF6C5CE7),
onPrimary: Color(0xFFFFFFFF),
accent: Color(0xFF00CEC9),
onAccent: Color(0xFFFFFFFF),
surface: Color(0xFFFFFFFF),
onSurface: Color(0xFF111111),
textPrimary: Color(0xFF111111),
textSecondary: Color(0xFF444444),
textTertiary: Color(0xFF777777),
error: Color(0xFFB3261E),
onError: Color(0xFFFFFFFF),
);
static const dark = AppColors(
primary: Color(0xFFB4A7FF),
onPrimary: Color(0xFF1A1A2E),
accent: Color(0xFF55EFC4),
onAccent: Color(0xFF003D33),
surface: Color(0xFF121212),
onSurface: Color(0xFFFFFFFF),
textPrimary: Color(0xFFFFFFFF),
textSecondary: Color(0xFFCCCCCC),
textTertiary: Color(0xFF999999),
error: Color(0xFFF2B8B5),
onError: Color(0xFF601410),
);
}

The on* roles are the foreground used on top of their counterpart. If primary is your button fill, onPrimary is the label on that button.

A dark theme needs a dark palette. Passing a light palette with Brightness.dark darkens Material’s own ColorScheme, but your semantic colors stay light — and the result is black text on a white surface with no error to tell you why.

// Wrong: dark brightness, light palette
FabrikTheme.create(
brightness: Brightness.dark,
colors: AppPalette.light, // context.colors.surface is still white
);
// Right
FabrikTheme.create(
brightness: Brightness.dark,
colors: AppPalette.dark,
);

error and onError are part of the palette, so failures stay inside the design system instead of reaching for Colors.red:

Text(
'Card was declined',
style: context.typography.bodyMedium.copyWith(
color: context.colors.error,
),
);

These roles are also fed into Material’s ColorScheme, so TextField error borders and your own error text use the same color.

Both are optional in the constructor and fall back to ColorTokens.error and ColorTokens.onError, so palettes written before this release keep working.

Pass fontFamily and it is applied across every generated text style:

FabrikTheme.create(
brightness: Brightness.light,
colors: AppPalette.light,
fontFamily: 'Inter',
);

If you pass your own AppTypography, fontFamily is ignored — your styles are used exactly as written.

AppTypography exposes semantic variants rather than one style per size. The naming follows a consistent pattern:

SuffixMeaningExample
(none)Default text colorbodyLarge
PrimaryBrand-colored, for accentsheadlineLargePrimary
SecondaryLower emphasisbodyMediumSecondary
TertiaryLowest emphasis, helper textbodySmallTertiary
EmphasisSame color, heavier weightbodyLargeEmphasis
Text('Order #1042', style: context.typography.titleMedium)
Text('Delivered', style: context.typography.bodyMediumSecondary)
Text('48.00', style: context.typography.bodyLargeEmphasis)

Builds a complete ThemeData with both extensions registered.

ParameterTypeDefaultDescription
brightnessBrightnessrequiredLight or dark
colorsAppColorsrequiredYour semantic palette
typographyAppTypography?derivedOverrides the generated typography
fontFamilyString?nullApplied to generated typography only
RolePurpose
primary / onPrimaryKey actions and emphasis
accent / onAccentSecondary emphasis
surface / onSurfaceBackgrounds and content on them
textPrimaryHigh-emphasis text
textSecondarySupporting text
textTertiaryHelper and placeholder text
error / onErrorFailures and destructive actions

Factories: AppColors.defaults() (light), AppColors.darkDefaults() (dark). Both implement copyWith, lerp, == and hashCode.

Raw values used to compose semantic roles. Access them directly for layout work — they do not require a BuildContext.

Token classScale
SpacingTokensnone xxs xs sm md lg xl xxl xxxl (0–48)
RadiusTokensnone xs sm md lg xl full (0–999)
ElevationTokensnone xs sm md lg xl (0–24)
BorderTokensnone thin medium thick (0–2)
ColorTokensBase palette, light and dark variants
TypographyTokensFont sizes, line heights, letter spacing
Padding(
padding: const EdgeInsets.all(SpacingTokens.lg),
child: Card(
elevation: ElevationTokens.sm,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(RadiusTokens.md),
),
child: content,
),
);
GetterReturnsThrows
context.colorsAppColorsStateError if the theme was not built with FabrikTheme.create
context.typographyAppTypographyStateError, as above

Both throw in release builds as well as debug, so a missing theme fails with a readable message rather than a null dereference.