dtcg_theme 0.7.1 copy "dtcg_theme: ^0.7.1" to clipboard
dtcg_theme: ^0.7.1 copied to clipboard

Turns W3C Design Tokens (DTCG) files into a type-safe Flutter theme: a ThemeExtension per mode, and a build that fails on a broken reference or a pair below your WCAG target.

example/lib/main.dart

import 'package:flutter/material.dart';

import 'theme/app_tokens.g.dart';

void main() => runApp(const ExampleApp());

/// Builds a [ThemeData] out of nothing but generated tokens.
ThemeData themeFrom(AppTokens tokens) {
  return ThemeData(
    scaffoldBackgroundColor: tokens.colorSurfaceSunken,
    // Generated from the `color_scheme` and `text_theme` blocks of dtcg.yaml.
    colorScheme: tokens.colorScheme,
    textTheme: tokens.textTheme,
    // The tokens travel with the theme, so any widget can read them.
    extensions: <ThemeExtension<dynamic>>[tokens],
  );
}

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

  @override
  State<ExampleApp> createState() => _ExampleAppState();
}

class _ExampleAppState extends State<ExampleApp> {
  bool _dark = false;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'dtcg_theme example',
      theme: themeFrom(AppTokens.light),
      darkTheme: themeFrom(AppTokens.dark),
      themeMode: _dark ? ThemeMode.dark : ThemeMode.light,
      // The duration and the curve are tokens too, so the crossfade between
      // light and dark is part of the design system.
      // One `transition` token carries both halves, so they cannot drift.
      themeAnimationDuration: AppTokens.light.motionTheme.duration,
      themeAnimationCurve: AppTokens.light.motionTheme.curve,
      home: HomePage(
        dark: _dark,
        onToggle: (bool value) => setState(() => _dark = value),
      ),
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({super.key, required this.dark, required this.onToggle});

  final bool dark;
  final ValueChanged<bool> onToggle;

  @override
  Widget build(BuildContext context) {
    final AppTokens tokens = AppTokens.of(context);

    return Scaffold(
      appBar: AppBar(
        title: Text('dtcg_theme', style: tokens.textTitle),
        backgroundColor: tokens.colorSurface,
        foregroundColor: tokens.colorOnSurface,
        actions: <Widget>[
          Switch(value: dark, onChanged: onToggle),
          SizedBox(width: tokens.spaceMd),
        ],
      ),
      body: ListView(
        padding: EdgeInsets.all(tokens.spaceLg),
        children: <Widget>[
          Container(
            padding: EdgeInsets.all(tokens.spaceLg),
            decoration: BoxDecoration(
              gradient: tokens.brandHero,
              borderRadius: BorderRadius.circular(tokens.radiusMd),
            ),
            child: Text(
              'brand.hero',
              style: tokens.textTitle.copyWith(color: tokens.colorOnPrimary),
            ),
          ),
          SizedBox(height: tokens.spaceLg),
          Text('Everything here comes from tokens', style: tokens.textTitle),
          SizedBox(height: tokens.spaceSm),
          Text(
            'Colours, spacing, radii, the type scale, the shadow and even the '
            'theme crossfade are values generated from the DTCG files in '
            'tokens/. No hex codes and no magic numbers in this file.',
            style: tokens.textBody.copyWith(color: tokens.colorMuted),
          ),
          SizedBox(height: tokens.spaceLg),
          const _GlowBadge(),
          SizedBox(height: tokens.spaceLg),
          const _TokenCard(),
          SizedBox(height: tokens.spaceLg),
          Text('Palette', style: tokens.textTitle),
          SizedBox(height: tokens.spaceSm),
          const _Swatches(),
        ],
      ),
    );
  }
}

class _TokenCard extends StatelessWidget {
  const _TokenCard();

  @override
  Widget build(BuildContext context) {
    final AppTokens tokens = AppTokens.of(context);

    return Container(
      padding: EdgeInsets.all(tokens.spaceLg),
      decoration: BoxDecoration(
        color: tokens.colorSurface,
        borderRadius: BorderRadius.circular(tokens.radiusMd),
        border: Border.fromBorderSide(tokens.strokeDivider),
        boxShadow: tokens.elevationCard,
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Text('elevation.card + stroke.divider', style: tokens.textBody),
          SizedBox(height: tokens.spaceSm),
          Text(
            'Colour, radius, border, shadow and spacing, all from tokens. '
            'The contrast of every text pair here was checked at build time.',
            style: tokens.textBody.copyWith(color: tokens.colorMuted),
          ),
          SizedBox(height: tokens.spaceMd),
          FilledButton(
            onPressed: () {},
            style: FilledButton.styleFrom(
              backgroundColor: tokens.colorPrimary,
              foregroundColor: tokens.colorOnPrimary,
              padding: EdgeInsets.symmetric(
                horizontal: tokens.spaceLg,
                vertical: tokens.spaceSm,
              ),
            ),
            child: Text('color.primary', style: tokens.textBody),
          ),
        ],
      ),
    );
  }
}

/// A radial gradient, which the tokens ask for through `$extensions`.
class _GlowBadge extends StatelessWidget {
  const _GlowBadge();

  @override
  Widget build(BuildContext context) {
    final AppTokens tokens = AppTokens.of(context);

    return Container(
      height: 120,
      alignment: Alignment.center,
      decoration: BoxDecoration(
        gradient: tokens.brandGlow,
        borderRadius: BorderRadius.circular(tokens.radiusMd),
        border: Border.all(color: tokens.colorOutline),
      ),
      child: Text(
        'brand.glow (radial)',
        style: tokens.textBody.copyWith(color: tokens.colorOnSurface),
      ),
    );
  }
}

class _Swatches extends StatelessWidget {
  const _Swatches();

  @override
  Widget build(BuildContext context) {
    final AppTokens tokens = AppTokens.of(context);
    final Map<String, Color> swatches = <String, Color>{
      'surface': tokens.colorSurface,
      'on-surface': tokens.colorOnSurface,
      'primary': tokens.colorPrimary,
      'muted': tokens.colorMuted,
      'outline': tokens.colorOutline,
      // Declared in oklch, converted to sRGB by the generator.
      'accent (oklch)': tokens.paletteAccent,
    };

    return Wrap(
      spacing: tokens.spaceMd,
      runSpacing: tokens.spaceMd,
      children: <Widget>[
        for (final MapEntry<String, Color> entry in swatches.entries)
          Column(
            children: <Widget>[
              Container(
                width: 64,
                height: 64,
                decoration: BoxDecoration(
                  color: entry.value,
                  borderRadius: BorderRadius.circular(tokens.radiusMd),
                  border: Border.all(color: tokens.colorOutline),
                ),
              ),
              SizedBox(height: tokens.spaceSm),
              Text(entry.key, style: tokens.textBody),
            ],
          ),
      ],
    );
  }
}
0
likes
160
points
109
downloads

Documentation

API reference

Publisher

verified publisherbomsamdi.com

Weekly Downloads

Turns W3C Design Tokens (DTCG) files into a type-safe Flutter theme: a ThemeExtension per mode, and a build that fails on a broken reference or a pair below your WCAG target.

Repository (GitHub)
View/report issues

Topics

#design-tokens #theme #codegen #figma #accessibility

License

MIT (license)

Dependencies

args, yaml

More

Packages that depend on dtcg_theme