Skip to content

Theme Configuration

View raw .md

Configure colours, spacing, typography, breakpoints and animations through WindThemeData, and wrap the app in WindTheme.

On this page
Preview core-concepts/theming_example A live demo of this example follows. theming_example.dart View source Open demo

Introduction#

Wind is designed to be fully customisable from the ground up. If you've ever used Tailwind CSS, you'll feel right at home with how we handle "design tokens." Instead of hunting through nested widget properties to change a colour or a font size, you define your design system once in your theme configuration.

Everything in Wind (from the colours in bg-blue-500 to the spacing in p-4) is driven by WindThemeData. This ensures that your UI remains consistent across the entire application while giving you the freedom to break away from defaults whenever you need to.

The WindTheme Widget#

To start customising your design system, you need to wrap your application with the WindTheme widget. This widget acts as a provider, making your theme configuration available to all W widgets in the tree.

Let's look at a basic setup:

dart
import 'package:wind_kit/wind_kit.dart';

void main() {
  runApp(
    WindTheme(
      data: WindThemeData(
        // We'll customize this in the next sections
      ),
      onThemeChanged: (brightness) {
        // Optional: persist user's theme preference
      },
      child: MaterialApp(
        home: MyHome(),
      ),
    ),
  );
}

Theme Change Callbacks#

When a user manually toggles the theme via toggleTheme(), you may want to persist their preference. The onThemeChanged callback fires on every brightness change that does not come from the OS sync; it does not fire when the system brightness changes automatically.

dart
WindTheme(
  onThemeChanged: (brightness) {
    // Persist the manual choice with your own storage (savePreference is the host's).
    savePreference('theme_mode', brightness == Brightness.dark ? 'dark' : 'light');
  },
  data: WindThemeData(),
  child: MaterialApp(
    home: MyHome(),
  ),
)

The callback receives only the brightness, so a stored light or dark means "the user chose this mode", not "the OS currently reports it". Restore it as a manual choice with syncWithSystem: false, and store nothing while the user follows the system (see Resetting to System Theme).

Resetting to System Theme#

After a user manually toggles the theme, the automatic system sync is disabled to respect their choice. To re-enable automatic system brightness sync, call resetToSystem():

dart
// Re-enable system brightness sync
WindTheme.of(context).resetToSystem();

// Or via extension
context.windTheme.resetToSystem();

This immediately syncs the theme with the current platform brightness and re-enables the syncWithSystem flag so future OS changes are reflected automatically.

If you persist the preference, clear it after you reset. onThemeChanged fires after resetToSystem() only when the brightness value actually changes (and then it saves the platform brightness as if it were a manual choice), and never for later OS changes, so a stored light or dark cannot tell a restart that the user went back to following the system. Delete the stored value yourself once the reset has run, and restore from it on startup:

dart
// Going back to the system: re-enable sync, then forget the manual choice
// (after the reset, because the reset itself may fire onThemeChanged).
context.windTheme.resetToSystem();
await removePreference('theme_mode');

// On startup: no stored value keeps the default syncWithSystem: true.
final String? saved = await readPreference('theme_mode');

WindTheme(
  data: saved == null
      ? WindThemeData()
      : WindThemeData(
          brightness: saved == 'dark' ? Brightness.dark : Brightness.light,
          syncWithSystem: false,
        ),
  onThemeChanged: (brightness) {
    savePreference('theme_mode', brightness == Brightness.dark ? 'dark' : 'light');
  },
  child: MaterialApp(
    home: MyHome(),
  ),
)

removePreference, readPreference and savePreference stand for the host's own storage.

Colors#

Colours in Wind are defined as palettes. When you add a colour to your theme, Wind expects a MaterialColor so it can resolve utilities like bg-brand-50 through bg-brand-900.

Here's how to define custom colours:

dart
WindThemeData(
  colors: {
    // A MaterialColor swatch: shades 50-900 (Flutter swatches have no 950)
    'brand': Colors.indigo,

    // Explicit shades for fine-tuned control
    'accent': MaterialColor(0xFF3B82F6, {
      50: Color(0xFFEFF6FF),
      500: Color(0xFF3B82F6),
      900: Color(0xFF1E3A8A),
    }),
  },
)

Once defined, these become available as utility classes immediately: text-brand-600, bg-accent-500/50, or border-brand-900.

Typography#

You can take full control of your app's typography by overriding font families, sizes, and weights. Wind applies a "sans" font family globally by default, mimicking Tailwind's behaviour.

dart
WindThemeData(
  fontFamilies: {
    'sans': 'Inter',      // The default global font
    'display': 'Oswald',   // Accessible via font-display
  },
  fontSizes: {
    'xs': 12.0,
    'base': 16.0,
    'xl': 20.0,
    '4xl': 36.0,
  },
  fontWeights: {
    'thin': FontWeight.w100,
    'bold': FontWeight.w700,
  },
)

But what if you don't want Wind to inject a global DefaultTextStyle? You can simply set applyDefaultFontFamily: false in your configuration.

Spacing and Sizing#

Wind uses a numeric scale for spacing (p-4, m-2, gap-6). By default, each unit is equal to 4.0 logical pixels. This means p-4 translates to 16.0 pixels.

If your design system is built on a different grid (like a 5px or 8px grid), you can adjust the baseSpacingUnit:

dart
WindThemeData(
  baseSpacingUnit: 5.0, // Now p-4 = 20px
)

You can also customise the "screen" breakpoints if the default Tailwind values don't fit your needs:

dart
WindThemeData(
  screens: {
    'sm': 600,
    'md': 900,
    'lg': 1200,
  },
)

Borders and Shadows#

The "feel" of your app often comes down to its borders and shadows. Wind allows you to define these scales explicitly so your rounded-lg or shadow-xl classes are always consistent.

dart
WindThemeData(
  borderRadius: {
    'none': 0,
    'sm': 2,
    'DEFAULT': 4, // Applied by 'rounded' class
    'lg': 8,
    'full': 9999,
  },
  shadows: {
    'sm': [BoxShadow(color: Colors.black12, blurRadius: 2)],
    'md': [BoxShadow(color: Colors.black26, blurRadius: 6, offset: Offset(0, 2))],
  },
)

Customizing Defaults#

Wind provides a comprehensive default theme that matches Tailwind CSS v3. When you provide your own WindThemeData, your values are merged with the defaults.

If you want to tweak just one or two things from an existing configuration, use copyWith:

dart
final darkTheme = myDefaultTheme.copyWith(
  brightness: Brightness.dark,
  ringColor: Colors.amber,
);

Aliases#

WindThemeData(aliases: {...}) lets you define short, bare-token names that expand to full utility strings before parsing. Every W widget resolves aliases centrally, so a class name like row becomes flex flex-row everywhere without any per-widget wiring.

dart
WindThemeData(
  aliases: {
    'row':   'flex flex-row',
    'col':   'flex flex-col',
    'center': 'items-center justify-center',
    'row-c': 'row center',      // recursive: expands 'row' and 'center' first
  },
)

With this configuration, the following className values are equivalent:

dart
// Before aliases
WDiv(className: 'flex flex-row items-center justify-center bg-white dark:bg-gray-800')

// After aliases
WDiv(className: 'row-c bg-white dark:bg-gray-800')

How expansion works:

  • Alias keys are always bare (no prefix). A prefixed token still resolves: its prefix is peeled off, its bare body is matched against the map, and the prefix is re-applied to every produced token. md:row becomes md:flex md:flex-row, and hover:bg-surface carries hover: onto each token the bg-surface alias produces (including its dark: peer, yielding hover:dark:..., which the parser resolves regardless of prefix order). Define keys bare (row, bg-surface), never md:row.
  • Expansion is recursive: an alias value may reference other aliases. The expander resolves all aliases before handing the result to the parser.
  • Aliases are empty by default ({}). The feature is purely opt-in: a WindThemeData without an aliases key behaves identically to today.
  • If an alias key shadows a built-in token (for example, 'flex': 'flex flex-row'), the alias wins and Wind emits a debug-mode warning so you can rename before shipping.
  • Expansion is bounded: a cyclic alias ('a': 'a') and a deep or wide fan-out map terminate safely (the offending token is left unexpanded or the output is capped), so a misconfigured map never hangs rendering. Aliases are developer configuration, not a place to interpolate untrusted runtime strings.

Quick Reference#

WindThemeData exposes 24 configurable fields. The table below groups them by concern.

Mode and Behavior#

Property Type Default Description
brightness Brightness light Initial mode (light or dark). Overridden on mount by the OS brightness while syncWithSystem is true (see note below)
syncWithSystem bool true Auto-follow OS brightness until the user calls toggleTheme()
applyDefaultFontFamily bool true Inject Wind's default font family as a global DefaultTextStyle
baseSpacingUnit double 4.0 Multiplier for numeric spacing (p-4 → 4 * 4 = 16px)

Setting brightness: Brightness.dark alone has no effect while syncWithSystem is true (the default): the controller reads the OS brightness on mount and overrides it, so dark: classes stay inactive on a light OS. To pin a fixed mode, pass WindThemeData(brightness: Brightness.dark, syncWithSystem: false), or switch at runtime with controller.toggleTheme() / setTheme(...).

Tokens#

Property Type Description
colors Map<String, MaterialColor> Custom colour palettes (bg-brand-500, etc.)
screens Map<String, int> Breakpoint min-widths (sm, md, lg, xl, 2xl, custom)
containers Map<String, int> Named spacing: p-md, gap-xs resolve to containers[key] * baseSpacingUnit. There is no container utility, and max-w-* uses a fixed scale.
fontFamilies Map<String, String> Font aliases (sans, serif, mono, custom)
fontSizes Map<String, double> Text size scale (text-xs through text-6xl)
fontWeights Map<String, FontWeight> Weight scale (font-thin through font-black)
tracking Map<String, double> Letter spacing in em, a multiple of the font size (tracking-tight, etc.)
leading Map<String, double> Line height (leading-tight, etc.)
borderWidths Map<String, double> Border width scale (border-2, etc.)
borderRadius Map<String, double> Corner radius scale (rounded-lg, etc.)
shadows Map<String, List<BoxShadow>> Shadow definitions (shadow-md, etc.)
opacities Map<String, double> Opacity scale (opacity-50, etc.)
zIndices Map<String, int> Z-index scale (z-10, etc.)
transitionDurations Map<String, Duration> Duration scale (duration-200, etc.)
transitionCurves Map<String, Curve> Easing scale (ease-in-out, etc.)
animations Map<String, WindAnimationType> Animation types (animate-spin, etc.)
aliases Map<String, String> Bare-token shorthands that expand (recursively) before parsing. Default {}. See Aliases.

Ring (Focus) Effects#

Property Type Default Description
ringColor Color Tailwind blue-500 Default ring colour when not overridden by ring-{color}
ringWidths Map<String, double> 0,1,2,DEFAULT=3,4,8 Ring width scale (ring, ring-2, etc.)
ringOffsets Map<String, double> 0,1,2,4,8 Ring offset scale (ring-offset-2, etc.)

Controller methods#

WindTheme.of(context) returns a WindThemeController exposing these mutators:

Method Signature Description
toggleTheme() void toggleTheme() Flip between light and dark, pinning syncWithSystem to false.
setTheme() void setTheme(WindThemeData newData) Replace the active theme data wholesale.
updateTheme() void updateTheme({Brightness? brightness, Map<String, MaterialColor>? colors, Map<String, int>? screens, Map<String, double>? fontSizes, Map<String, FontWeight>? fontWeights, Map<String, String>? fontFamilies, Map<String, double>? borderWidths, Map<String, double>? borderRadius, double? baseSpacingUnit}) Partial update via copyWith; only the passed fields change.
resetToSystem() void resetToSystem() Re-enable automatic OS brightness sync.

Equality#

WindThemeData implements value-based == and hashCode (the hashCode covers scalar fields only), so constructing a fresh default WindThemeData() during a parent rebuild does not clobber a toggleTheme() choice already held by the controller.

Widget-level callback#

onThemeChanged is a WindTheme widget parameter, not a WindThemeData field. It fires on every brightness change that does not come from the OS sync (toggleTheme(), setTheme(), updateTheme(brightness:), resetToSystem(), and a new data: from a parent rebuild) and is documented in the Theme Change Callbacks section above.

For more details on how to sync these values with Flutter's standard Material components, check out the Theme Binding guide.

esc

Search index did not load.

No match for "".

Try a class like rounded-lg or a widget like WButton.