Skip to content

Theming

Kenny Mochizuki Escalona edited this page Aug 21, 2026 · 4 revisions

Theming

The layrz_ui theme system provides semantic access to all design tokens — colors, typography, spacing, shadows, borders, and motion. This page explains how the theme is structured and how to use it in widgets.


Overview

The theme system consists of three layers:

  1. LayrzTheme — the InheritedTheme widget that propagates theme data down the widget tree
  2. LayrzThemeData — an immutable container holding all tokens and icon theme settings
  3. LayrzTokens — the complete set of design values (colors, spacing, radius, shadows, etc.)

Every layrz_ui app installs a LayrzTheme via LayrzApp. Once installed, widgets access the theme via BuildContext.theme, BuildContext.tokens, or BuildContext.tokenizer.


LayrzTheme: Inheritance and Overlays

LayrzTheme extends InheritedTheme (not plain InheritedWidget) for a specific reason: to survive route and overlay boundaries.

Why InheritedTheme?

When Flutter crosses an overlay boundary (dialogs, tooltips, dropdowns, menus) or a route boundary, it creates a new widget tree context. Without special handling, an InheritedWidget would be lost, and LayrzTheme.of(context) would fail inside dialogs.

InheritedTheme implements a wrap() method that re-installs the theme below the boundary. This means:

showDialog(
  context: context,
  builder: (dialogContext) {
    // LayrzTheme is accessible here, even though it's in an overlay
    final theme = LayrzTheme.of(dialogContext);
    return MyDialog();
  },
);

This works because LayrzApp wraps the app's root, and InheritedTheme ensures the theme survives every route and overlay.

Accessing the Theme

Two static methods provide access:

// Throws if no LayrzTheme is found
final theme = LayrzTheme.of(context);

// Returns null if no LayrzTheme is found
final theme = LayrzTheme.maybeOf(context);

LayrzThemeData: The Container

LayrzThemeData is an immutable holder of all design values. It is typically not used directly — widgets use the convenience extensions on BuildContext instead.

Structure

@immutable
class LayrzThemeData {
  /// All color, typography, spacing, radius, shadow, border, and motion tokens.
  final LayrzTokens tokens;

  /// Icon theme (color, size) applied at the root.
  final IconThemeData iconTheme;

  // Delegating getters:
  Color get primaryColor => tokens.colors.primary;
  Color get backgroundColor => tokens.colors.sf1;
  Color get surfaceColor => tokens.colors.sf1;
  TextStyle get textStyle => tokens.typography.body;
  // ... and more
}

Creating a Theme

Use the factory constructor:

LayrzThemeData.light({
  Color primaryColor = kPrimaryColor,
  LayrzFont titleFont = kLayrzFont,
  LayrzFont bodyFont = kLayrzFont,
  LayrzFontHandler? fontHandler,
})

This wires all derived tokens consistently:

  • Color tokens are created first
  • Spacing and radius are independent
  • Shadow tokens are seeded with the surface color and base radius
  • Typography is seeded with the text color and font specifications
  • Motion tokens are independent

Customizing the Theme

// Create a light theme with a custom primary color
final theme = LayrzThemeData.light(
  primaryColor: const Color(0xFF0077BE), // Custom brand blue
);

// Or modify an existing theme
final customTheme = theme.copyWith(
  tokens: theme.tokens.copyWith(
    colors: theme.tokens.colors.copyWith(
      primary: const Color(0xFF0077BE),
    ),
  ),
);

LayrzTokens: The Complete Token Set

LayrzTokens is the aggregate of all semantic design values:

Member Type Contents
colors LayrzColorTokens Brand, surface, foreground, semantic, and structural colors
typography LayrzTextTheme Five text styles (display, headline, title, body, label)
spacing LayrzSpacingTokens Five semantic levels (sp1–sp5: 4, 8, 16, 24, 32 logical pixels) plus derived pdN/mgN getters
radius LayrzRadiusTokens Five semantic levels (r1–r5: 4, 8, 16, 24, 32 logical pixels) plus full (999, pill), derived brN getters, and innerRadius()
shadow LayrzShadowTokens Elevation levels 0–5 with computed blur and opacity
border LayrzBorderTokens Stroke widths and pre-built border sides
motion LayrzMotionTokens Durations and easing curves for animations

Access these directly:

final colors = context.tokens.colors;
final spacing = context.tokens.spacing;
final textStyle = context.tokens.typography.body;
final shadow = context.tokens.shadow.elevation2;

Using the Theme in Widgets

Pattern 1: Via BuildContext Extensions (Recommended)

The cleanest pattern uses extension methods on BuildContext:

class MyCard extends StatelessWidget {
  const MyCard({super.key});

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: context.theme.surfaceColor,
        borderRadius: BorderRadius.circular(context.theme.borderRadius),
        boxShadow: context.tokens.shadow.elevation1,
      ),
      padding: context.tokens.spacing.pd3,  // sp3 = 16 logical pixels
      child: Text(
        'Card content',
        style: context.theme.textStyle,
      ),
    );
  }
}

Pattern 2: Direct Access

You can also access the theme directly if you prefer:

final theme = LayrzTheme.of(context);
final primaryColor = theme.primaryColor;

Pattern 3: The Tokenizer Facade

For common values, the tokenizer provides flat shortcuts:

final primary = context.tokenizer.primary;
final spacing = context.tokenizer.spacing; // 8.0 (base unit)
final allSpacing = context.tokenizer.spacingTokens; // Full set

See LayrzTokenizer for all available shortcuts.


Delegating Getters: Backwards Compatibility

LayrzThemeData provides delegating getters for the most-used values, maintained for backwards compatibility with layrz_theme:

Getter Returns Equivalent
primaryColor Color tokens.colors.primary
backgroundColor Color tokens.colors.sf1
surfaceColor Color tokens.colors.sf1
textColor Color tokens.colors.fg1
hintColor Color tokens.colors.fg3
borderColor Color tokens.colors.divider
dangerColor Color tokens.colors.danger
successColor Color tokens.colors.success
warningColor Color tokens.colors.warning
textTheme LayrzTextTheme tokens.typography
textStyle TextStyle tokens.typography.body
borderRadius double tokens.radius.r2 (8.0 pixels) — deprecated getter; use tokens.radius.br2 for BorderRadius directly
iconTheme IconThemeData The icon theme (color, size)

New code should access tokens directly. These getters exist to prevent breaking existing call sites.


Icons

Every layrz_ui app installs an IconTheme via LayrzApp. The default icon theme uses:

  • Color: tokens.colors.fg1 (primary text color)
  • Size: 24 logical pixels

Icons render in this color and size unless overridden:

Icon(Icons.star) // Renders at 24px in fg1 color (dark navy text)
Icon(Icons.star, size: 32, color: context.theme.primaryColor) // Override

Text Styling

The theme provides five core text styles via tokens.typography. Each style serves a specific purpose and does not offer size variants — developers needing a variant use copyWith(fontSize:) to be explicit:

Style Size Weight Use Case
display 40px w700 Hero text and splash screens
headline 24px w600 Page-level headings
title 20px w600 Dialog and card titles
body 16px w400 Body text and reading passages
label 14px w400 Labels, buttons, tooltips, badges
Text(
  'Section Heading',
  style: context.tokens.typography.headline,
)

Advanced: Customizing Tokens at Runtime

To change the theme at runtime, rebuild LayrzApp with a new theme:

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  Color _brandColor = kPrimaryColor;

  void _changeBrandColor(Color newColor) {
    setState(() {
      _brandColor = newColor;
    });
  }

  @override
  Widget build(BuildContext context) {
    return LayrzApp(
      title: 'My App',
      theme: LayrzThemeData.light(primaryColor: _brandColor),
      home: HomePage(onChangeColor: _changeBrandColor),
    );
  }
}

When you call setState, LayrzApp rebuilds with the new theme, and all descendants receive the updated tokens.


Related Decisions

  • D7: Light Mode Only — Explains why dark mode is not supported and why the theme system is designed for light mode only.

Last updated: 2026-08-13
Related pages: Design-Tokens, LayrzTokenizer, LayrzApp, Getting-Started

Clone this wiki locally