Variants & Tones
Both the variant system (structural styles — Button, Chip, Badge) and the
tone system (semantic colour intent — Alert, Card, Snackbar, ProgressBar,
StatusDot) use exactly the same underlying pattern: an abstract class with a
resolve(ColorScheme) method that returns a typed style record.
The only distinction between the two is naming:
- Variant (
ButtonVariant,ChipVariant,BadgeVariant) — controls structure and emphasis. - Tone (
AlertTone,CardTone,SnackbarTone,ProgressBarTone,StatusTone) — controls semantic colour intent.
The pattern
Every variant or tone is built from three pieces:
- A style record — an
@immutableclass holding only the resolved visual properties the component actually needs (colours, borders, shadows, and so on). - An abstract class — exposes built-in options as
static constfields and declares theresolve(ColorScheme)method that subclasses must implement. - Private built-in implementations — one small private class per
built-in preset, each returning a style record computed from the
ColorSchemepassed toresolve.
abstract class FooVariant { // or FooTone
const FooVariant();
static const FooVariant primary = _PrimaryFooVariant();
FooVariantStyle resolve(ColorScheme colorScheme);
}
class FooVariantStyle {
const FooVariantStyle({
required this.foregroundColor,
this.backgroundColor,
// … only the fields this component actually needs
});
final Color foregroundColor;
final Color? backgroundColor;
}
class _PrimaryFooVariant extends FooVariant {
const _PrimaryFooVariant();
FooVariantStyle resolve(ColorScheme cs) => FooVariantStyle(
foregroundColor: cs.onBrand,
backgroundColor: cs.brand,
);
}
Users interact only with the static constants (FooVariant.primary) or their
own subclasses — the private _PrimaryFooVariant-style implementations never
appear in the public API.
Worked example: ButtonVariant
The Button component (see lib/src/components/atoms/button.dart) is the
canonical example. ButtonVariantStyle holds exactly what a button needs to
render — no more:
class ButtonVariantStyle {
const ButtonVariantStyle({
required this.foregroundColor,
this.backgroundColor,
this.borderColor,
this.shadows,
});
final Color foregroundColor;
final Color? backgroundColor;
final Color? borderColor;
final List<BoxShadow>? shadows;
}
ButtonVariant is the abstract class. It exposes six built-in presets as
static constants — primary, secondary, tertiary, ghost,
destructive, success — and declares the resolve contract:
abstract class ButtonVariant {
const ButtonVariant();
static const ButtonVariant primary = _PrimaryButtonVariant();
static const ButtonVariant secondary = _SecondaryButtonVariant();
static const ButtonVariant tertiary = _TertiaryButtonVariant();
static const ButtonVariant ghost = _GhostButtonVariant();
static const ButtonVariant destructive = _DestructiveButtonVariant();
static const ButtonVariant success = _SuccessButtonVariant();
ButtonVariantStyle resolve(ColorScheme colorScheme);
}
Each preset is a private class resolving to a style record built from the
ColorScheme. For example, the primary variant:
class _PrimaryButtonVariant extends ButtonVariant {
const _PrimaryButtonVariant();
ButtonVariantStyle resolve(ColorScheme cs) {
return ButtonVariantStyle(
backgroundColor: cs.brand,
foregroundColor: cs.onBrand,
shadows: [
BoxShadow(
offset: const Offset(0, 6),
blurRadius: 18,
color: cs.brand.withValues(alpha: 0.28),
),
],
);
}
}
The Button widget itself simply calls resolve against the current theme's
colour scheme at build time:
final style = widget.variant.resolve(context.colorScheme);
Worked example: AlertTone
AlertTone follows the identical shape, but for semantic colour intent
rather than structure. AlertToneStyle only needs two colours:
class AlertToneStyle {
const AlertToneStyle({
required this.backgroundColor,
required this.accentColor,
});
final Color backgroundColor;
final Color accentColor;
}
AlertTone exposes four built-in presets — info, success, warning,
danger:
abstract class AlertTone {
const AlertTone();
static const AlertTone info = _InfoAlertTone();
static const AlertTone success = _SuccessAlertTone();
static const AlertTone warning = _WarningAlertTone();
static const AlertTone danger = _DangerAlertTone();
AlertToneStyle resolve(ColorScheme cs);
}
class _InfoAlertTone extends AlertTone {
const _InfoAlertTone();
AlertToneStyle resolve(ColorScheme cs) => AlertToneStyle(
backgroundColor: cs.infoSoft,
accentColor: cs.info,
);
}
Defining your own variant or tone
Because the abstract class is open for subclassing, you are not limited to
the built-in presets. Extend the abstract class and implement resolve:
class MyBrandVariant extends ButtonVariant {
const MyBrandVariant();
ButtonVariantStyle resolve(ColorScheme cs) {
return ButtonVariantStyle(
backgroundColor: cs.brand,
foregroundColor: cs.onBrand,
shadows: [
BoxShadow(
color: cs.brand.withValues(alpha: 0.35),
offset: const Offset(0, 4),
blurRadius: 12,
),
],
);
}
}
// Then use it exactly like a built-in preset:
Button(
label: const Text('Go'),
variant: const MyBrandVariant(),
onPressed: _handleGo,
)
The same applies to tones — subclass AlertTone (or CardTone,
SnackbarTone, ProgressBarTone, StatusTone) and implement resolve to
return whatever colours you need.
Custom factory for one-off values
Some tones also expose a static factory for a one-off colour that doesn't
warrant a named subclass. StatusTone, for example, exposes
StatusTone.custom:
abstract class StatusTone {
// …
static StatusTone custom(Color color) => _CustomStatusTone(color);
}
Call sites look identical whether you use a built-in preset, a custom factory, or your own subclass:
tone: AlertTone.success
tone: StatusTone.custom(const Color(0xFF9333EA))
tone: const MyMaintenanceTone()
Why abstract classes, not enums
Enums are closed — users cannot add cases to them. Abstract classes let
users subclass and implement resolve to produce whatever style they need,
while the library still ships sensible defaults as static constants. The
call site is identical either way: tone: AlertTone.success vs
tone: const MyTone().
Variants and tones in this library
- Variants — ButtonVariant, ChipVariant, BadgeVariant (see the Button, Chip, and Badge components).
- Tones — AlertTone, CardTone, SnackbarTone, ProgressBarTone, StatusTone (see the Alert, Card, Snackbar, ProgressBar, and StatusDot components).