material_design 1.0.1 copy "material_design: ^1.0.1" to clipboard
material_design: ^1.0.1 copied to clipboard

A Material Design 3 contract for Flutter. Enforces M3 tokens system-wide, replacing free primitives with type-safe M3 equivalents.

example/lib/main.dart

// Material Design 3 contract — the whole public API in one file.
//
// This example is deliberately a single file so you can read it top to bottom
// and copy any section straight into your app. The sections follow the same
// order as the README's "API tour":
//
//   1. Spacing & layout        6. Interaction states & focus
//   2. Shape & borders         7. Motion
//   3. Elevation & surfaces    8. Adaptive & responsive
//   4. Typography              9. Accessibility
//   5. Color                  10. M3 Expressive (experimental)
//                             11. Breaking the contract (M3Contract)
//
// Live version of everything here: https://fluttely.github.io/material_design/

import 'package:flutter/material.dart';
import 'package:material_design/material_design.dart';

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

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

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

class _ExampleAppState extends State<ExampleApp> {
  ThemeMode _mode = ThemeMode.light;

  ThemeData _theme(Brightness brightness) {
    // M3TextTheme.applyToTheme MERGES the 15 M3 text styles into the theme,
    // preserving the brightness-resolved colors the theme already computed.
    return M3TextTheme.applyToTheme(
      ThemeData(
        useMaterial3: true,
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF6750A4),
          brightness: brightness,
        ),
        visualDensity: M3VisualDensity.standard,
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'material_design example',
      debugShowCheckedModeBanner: false,
      theme: _theme(Brightness.light),
      darkTheme: _theme(Brightness.dark),
      themeMode: _mode,
      home: ExampleHomePage(
        onToggleTheme: () => setState(() {
          _mode = _mode == ThemeMode.light ? ThemeMode.dark : ThemeMode.light;
        }),
      ),
    );
  }
}

class ExampleHomePage extends StatelessWidget {
  const ExampleHomePage({required this.onToggleTheme, super.key});

  final VoidCallback onToggleTheme;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Material Design 3 — design contract'),
        actions: [
          IconButton(
            onPressed: onToggleTheme,
            icon: const Icon(Icons.brightness_6),
            tooltip: 'Toggle light/dark',
          ),
        ],
      ),
      body: ListView(
        // The page margin is itself a token: 16dp on compact screens.
        padding: const M3EdgeInsets.all(M3Margins.compactScreen),
        children: const [
          _SpacingSection(),
          _ShapeSection(),
          _ElevationSection(),
          _TypographySection(),
          _ColorSection(),
          _InteractionSection(),
          _MotionSection(),
          _AdaptiveSection(),
          _AccessibilitySection(),
          _ExpressiveSection(),
          _ContractSection(),
        ],
      ),
    );
  }
}

/// Shared section scaffold — a Card shaped, padded, and spaced by tokens only.
class _Section extends StatelessWidget {
  const _Section({required this.title, required this.children});

  final String title;
  final List<Widget> children;

  @override
  Widget build(BuildContext context) {
    return Card(
      shape: M3Shape.medium, // 12dp — the M3 card shape
      margin: const M3EdgeInsets.only(bottom: M3Spacings.s16),
      child: M3Padding(
        padding: M3EdgeInsetsPatterns.card, // 16dp all around
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(title, style: M3TypeScale.titleLarge),
            const M3Gap(M3Spacings.s16),
            ...children,
          ],
        ),
      ),
    );
  }
}

// ═════════════════════════════════════════════════════════════════════════
// 1. SPACING & LAYOUT — M3Spacings, M3EdgeInsets, M3Padding, M3Gap
// ═════════════════════════════════════════════════════════════════════════

class _SpacingSection extends StatelessWidget {
  const _SpacingSection();

  @override
  Widget build(BuildContext context) {
    final colorScheme = Theme.of(context).colorScheme;

    return _Section(
      title: '1. Spacing & layout',
      children: [
        // Every stop of the 4dp grid, straight from the scale's `values` list.
        for (final spacing in const [
          M3Spacings.s4,
          M3Spacings.s8,
          M3Spacings.s16,
          M3Spacings.s24,
          M3Spacings.s32,
          M3Spacings.s48,
        ])
          M3Padding(
            padding: const M3EdgeInsets.only(bottom: M3Spacings.s4),
            child: Row(
              children: [
                SizedBox(
                  width: M3Spacings.s48,
                  child: Text('${spacing.toInt()}dp',
                      style: M3TypeScale.labelMedium),
                ),
                Container(
                  width: spacing, // M3SpacingValue IS a double — no unwrap
                  height: M3Spacings.s16,
                  color: colorScheme.primary,
                ),
              ],
            ),
          ),
        const M3Gap(M3Spacings.s16), // vertical inside this Column
        Text(
          'M3Gap detects its orientation from the surrounding Row/Column. '
          'M3GapUtils.addGaps interleaves a whole list:',
          style: M3TypeScale.bodyMedium,
        ),
        const M3Gap(M3Spacings.s8),
        Row(
          children: M3GapUtils.addGaps(
            [
              const Chip(label: Text('one')),
              const Chip(label: Text('two')),
              const Chip(label: Text('three')),
            ],
            M3Spacings.s8,
          ),
        ),
      ],
    );
  }
}

// ═════════════════════════════════════════════════════════════════════════
// 2. SHAPE & BORDERS — M3Shape, M3BorderRadius, M3Border, M3BorderWidths
// ═════════════════════════════════════════════════════════════════════════

class _ShapeSection extends StatelessWidget {
  const _ShapeSection();

  static const _corners = <(String, M3BorderRadius)>[
    ('none 0', M3BorderRadius.none),
    ('xs 4', M3BorderRadius.extraSmall),
    ('sm 8', M3BorderRadius.small),
    ('md 12', M3BorderRadius.medium),
    ('lg 16', M3BorderRadius.large),
    ('xl 28', M3BorderRadius.extraLarge),
    ('full', M3BorderRadius.full),
  ];

  @override
  Widget build(BuildContext context) {
    final colorScheme = Theme.of(context).colorScheme;

    return _Section(
      title: '2. Shape & borders',
      children: [
        Text('The shape scale has exactly seven stops:',
            style: M3TypeScale.bodyMedium),
        const M3Gap(M3Spacings.s8),
        Wrap(
          spacing: M3Spacings.s8,
          runSpacing: M3Spacings.s8,
          children: [
            for (final (label, radius) in _corners)
              Container(
                width: M3Spacings.s64,
                height: M3Spacings.s48,
                alignment: Alignment.center,
                decoration: M3BoxDecoration(
                  color: colorScheme.surfaceContainerHighest,
                  borderRadius: radius,
                ),
                child: Text(label, style: M3TypeScale.labelSmall),
              ),
          ],
        ),
        const M3Gap(M3Spacings.s16),
        Text('Border widths are a scale too (0 / 1 / 2 / 4 dp):',
            style: M3TypeScale.bodyMedium),
        const M3Gap(M3Spacings.s8),
        Row(
          children: M3GapUtils.addGaps(
            [
              for (final (label, border) in <(String, M3Border)>[
                ('thin', M3Border.thin(colorScheme.outline)),
                ('thick', M3Border.thick(colorScheme.outline)),
                ('extraThick', M3Border.extraThick(colorScheme.outline)),
              ])
                Expanded(
                  child: Container(
                    padding: const M3EdgeInsets.all(M3Spacings.s12),
                    alignment: Alignment.center,
                    decoration: M3BoxDecoration(
                      border: border,
                      borderRadius: M3BorderRadius.small,
                    ),
                    child: Text(label, style: M3TypeScale.labelMedium),
                  ),
                ),
            ],
            M3Spacings.s8,
          ),
        ),
      ],
    );
  }
}

// ═════════════════════════════════════════════════════════════════════════
// 3. ELEVATION & SURFACES — M3Elevation, M3ElevationShadows, surface tint
// ═════════════════════════════════════════════════════════════════════════

class _ElevationSection extends StatelessWidget {
  const _ElevationSection();

  @override
  Widget build(BuildContext context) {
    return _Section(
      title: '3. Elevation & surfaces',
      children: [
        Text(
          'Each level carries its dp AND its shadows; the surface color is '
          'computed with the official tint formula.',
          style: M3TypeScale.bodyMedium,
        ),
        const M3Gap(M3Spacings.s8),
        Wrap(
          spacing: M3Spacings.s12,
          runSpacing: M3Spacings.s12,
          children: [
            for (final level in M3Elevation.values)
              Container(
                width: M3Spacings.s96,
                height: M3Spacings.s64,
                alignment: Alignment.center,
                decoration: M3BoxDecoration(
                  color: level.surfaceColor(context),
                  borderRadius: M3BorderRadius.medium,
                  boxShadow: level.shadows,
                ),
                child: Text(
                  'level ${level.index}\n${level.dp.toInt()}dp',
                  textAlign: TextAlign.center,
                  style: M3TypeScale.labelMedium,
                ),
              ),
          ],
        ),
      ],
    );
  }
}

// ═════════════════════════════════════════════════════════════════════════
// 4. TYPOGRAPHY — M3TypeScale (15 styles), M3TextUtils
// ═════════════════════════════════════════════════════════════════════════

class _TypographySection extends StatelessWidget {
  const _TypographySection();

  static const _styles = <(String, TextStyle)>[
    ('displaySmall 36', M3TypeScale.displaySmall),
    ('headlineSmall 24', M3TypeScale.headlineSmall),
    ('titleLarge 22', M3TypeScale.titleLarge),
    ('titleMedium 16', M3TypeScale.titleMedium),
    ('bodyLarge 16', M3TypeScale.bodyLarge),
    ('bodyMedium 14', M3TypeScale.bodyMedium),
    ('labelLarge 14', M3TypeScale.labelLarge),
    ('labelSmall 11', M3TypeScale.labelSmall),
  ];

  @override
  Widget build(BuildContext context) {
    return _Section(
      title: '4. Typography',
      children: [
        for (final (name, style) in _styles) Text(name, style: style),
        const M3Gap(M3Spacings.s16),
        Text(
          'M3TextUtils.mono — code-friendly variant of any style',
          style: M3TextUtils.mono(M3TypeScale.bodyMedium),
        ),
        Text(
          'M3TextUtils.dyslexiaFriendly — wider tracking, taller lines',
          style: M3TextUtils.dyslexiaFriendly(M3TypeScale.bodyMedium),
        ),
      ],
    );
  }
}

// ═════════════════════════════════════════════════════════════════════════
// 5. COLOR — M3TonalPalette, M3Tones, ColorScheme extensions, opacities
// ═════════════════════════════════════════════════════════════════════════

class _ColorSection extends StatelessWidget {
  const _ColorSection();

  @override
  Widget build(BuildContext context) {
    final colorScheme = Theme.of(context).colorScheme;
    // Real HCT tonal palette — the same math Material uses.
    final palette = M3TonalPalette.fromSeed(const Color(0xFF6750A4));

    return _Section(
      title: '5. Color',
      children: [
        Text('Tonal palette from seed, at the 13 M3 tone stops:',
            style: M3TypeScale.bodyMedium),
        const M3Gap(M3Spacings.s8),
        SizedBox(
          height: M3Spacings.s40,
          child: Row(
            children: [
              for (final tone in M3Tones.values)
                Expanded(child: Container(color: palette[tone])),
            ],
          ),
        ),
        const M3Gap(M3Spacings.s16),
        Wrap(
          spacing: M3Spacings.s8,
          runSpacing: M3Spacings.s8,
          children: [
            _colorChip(
              'hover state layer',
              colorScheme.stateLayerColor(
                colorScheme.primary,
                M3InteractionState.hover,
              ),
            ),
            _colorChip(
              'disabled content (38%)',
              colorScheme.disabledContent(colorScheme.onSurface),
            ),
            _colorChip(
              'surface @ level3',
              colorScheme.surfaceAtElevation(M3Elevation.level3),
            ),
          ],
        ),
      ],
    );
  }

  Widget _colorChip(String label, Color color) {
    return Chip(
      avatar: CircleAvatar(backgroundColor: color),
      label: Text(label, style: M3TypeScale.labelMedium),
    );
  }
}

// ═════════════════════════════════════════════════════════════════════════
// 6. INTERACTION STATES & FOCUS — M3StateLayer, M3FocusRing
// ═════════════════════════════════════════════════════════════════════════

class _InteractionSection extends StatelessWidget {
  const _InteractionSection();

  @override
  Widget build(BuildContext context) {
    final colorScheme = Theme.of(context).colorScheme;

    return _Section(
      title: '6. Interaction states & focus',
      children: [
        Text(
          'Hover / focus / press / drag overlays at official opacities '
          '(8% / 10% / 10% / 16%), with M3 precedence:',
          style: M3TypeScale.bodyMedium,
        ),
        const M3Gap(M3Spacings.s8),
        M3StateLayer(
          overlayColor: colorScheme.onSurface,
          borderRadius: M3BorderRadius.medium,
          onTap: () {},
          child: Container(
            padding: const M3EdgeInsets.all(M3Spacings.s16),
            decoration: M3BoxDecoration(
              color: colorScheme.surfaceContainerHigh,
              borderRadius: M3BorderRadius.medium,
            ),
            child:
                Text('Hover, press, or drag me', style: M3TypeScale.bodyLarge),
          ),
        ),
        const M3Gap(M3Spacings.s16),
        Text(
          'M3FocusRing reserves its 6dp inset permanently, so tabbing to the '
          'control never shifts the layout. Tab to see the 3dp ring:',
          style: M3TypeScale.bodyMedium,
        ),
        const M3Gap(M3Spacings.s8),
        M3FocusRing(
          borderRadius: M3BorderRadius.full,
          child: IconButton(
            onPressed: () {},
            icon: const Icon(Icons.star),
          ),
        ),
      ],
    );
  }
}

// ═════════════════════════════════════════════════════════════════════════
// 7. MOTION — M3Motion (duration + curve together)
// ═════════════════════════════════════════════════════════════════════════

class _MotionSection extends StatefulWidget {
  const _MotionSection();

  @override
  State<_MotionSection> createState() => _MotionSectionState();
}

class _MotionSectionState extends State<_MotionSection> {
  bool _expanded = false;

  @override
  Widget build(BuildContext context) {
    final colorScheme = Theme.of(context).colorScheme;

    return _Section(
      title: '7. Motion',
      children: [
        Text(
          'Duration and curve travel together — you never pair them by hand. '
          'This container animates with M3Motion.emphasized (500ms):',
          style: M3TypeScale.bodyMedium,
        ),
        const M3Gap(M3Spacings.s8),
        GestureDetector(
          onTap: () => setState(() => _expanded = !_expanded),
          child: AnimatedContainer(
            duration: M3Motion.emphasized.duration,
            curve: M3Motion.emphasized.curve,
            width: _expanded ? M3Spacings.s128 * 2 : M3Spacings.s128,
            height: M3Spacings.s64,
            alignment: Alignment.center,
            decoration: M3BoxDecoration(
              color: colorScheme.primaryContainer,
              borderRadius:
                  _expanded ? M3BorderRadius.extraLarge : M3BorderRadius.medium,
            ),
            child: Text('tap me', style: M3TypeScale.labelLarge),
          ),
        ),
        const M3Gap(M3Spacings.s16),
        Text(
          'Pick by intent: M3Motion.durationFor(M3MotionDistance.long) → '
          '${M3Motion.durationFor(M3MotionDistance.long).inMilliseconds}ms',
          style: M3TypeScale.bodyMedium,
        ),
      ],
    );
  }
}

// ═════════════════════════════════════════════════════════════════════════
// 8. ADAPTIVE & RESPONSIVE — M3ScreenSize, M3ResponsiveValue, M3Adaptive
// ═════════════════════════════════════════════════════════════════════════

class _AdaptiveSection extends StatelessWidget {
  const _AdaptiveSection();

  @override
  Widget build(BuildContext context) {
    final size = M3ScreenSize.of(context);

    return _Section(
      title: '8. Adaptive & responsive',
      children: [
        Text(
          'Window class: ${size.name} — ${size.columns} columns, '
          '${size.gutterWidth.toInt()}dp gutters, '
          '${size.pageMargin.toInt()}dp margins. Resize the window.',
          style: M3TypeScale.bodyMedium,
        ),
        const M3Gap(M3Spacings.s8),
        M3ResponsiveValue<String>(
          compact: 'compact: single column',
          medium: 'medium: rail + content',
          expanded: 'expanded: rail + content + panel',
          builder: (context, layout) =>
              Text(layout, style: M3TypeScale.labelLarge),
        ),
        const M3Gap(M3Spacings.s8),
        M3ResponsiveVisibility(
          visibleOn: const [
            M3ScreenSize.expanded,
            M3ScreenSize.large,
            M3ScreenSize.extraLarge,
          ],
          replacement: Text(
            '(a side panel would appear here on wider windows)',
            style: M3TypeScale.bodySmall,
          ),
          child: Text(
            'This side-panel text only exists on expanded+ windows.',
            style: M3TypeScale.bodySmall,
          ),
        ),
        const M3Gap(M3Spacings.s8),
        FilledButton(
          onPressed: () => M3Adaptive.showAdaptiveDialog<void>(
            context: context,
            title: 'Adaptive dialog',
            content: const Text(
              'Fullscreen on compact windows, AlertDialog elsewhere.',
            ),
            actions: [
              TextButton(
                onPressed: () => Navigator.of(context).pop(),
                child: const Text('Close'),
              ),
            ],
          ),
          child: const Text('M3Adaptive.showAdaptiveDialog'),
        ),
      ],
    );
  }
}

// ═════════════════════════════════════════════════════════════════════════
// 9. ACCESSIBILITY — M3Accessibility, WCAG helpers
// ═════════════════════════════════════════════════════════════════════════

class _AccessibilitySection extends StatelessWidget {
  const _AccessibilitySection();

  @override
  Widget build(BuildContext context) {
    final colorScheme = Theme.of(context).colorScheme;
    final contrast = M3ColorUtils.calculateContrast(
      colorScheme.onSurface,
      colorScheme.surface,
    );
    final passes = M3Accessibility.meetsContrastRequirement(
      foreground: colorScheme.onSurface,
      background: colorScheme.surface,
    );

    return _Section(
      title: '9. Accessibility',
      children: [
        Text(
          'onSurface on surface: contrast ${contrast.toStringAsFixed(2)}:1 — '
          '${passes ? 'passes' : 'fails'} WCAG AA (4.5:1).',
          style: M3TypeScale.bodyMedium,
        ),
        const M3Gap(M3Spacings.s8),
        Text(
          'Minimum touch target here: '
          '${M3Accessibility.minTouchTarget(context).toInt()}dp. '
          'Reduce motion requested: '
          '${M3Accessibility.shouldReduceMotion(context)}.',
          style: M3TypeScale.bodyMedium,
        ),
      ],
    );
  }
}

// ═════════════════════════════════════════════════════════════════════════
// 10. M3 EXPRESSIVE (experimental) — loading indicator, 35-shape library
// ═════════════════════════════════════════════════════════════════════════

class _ExpressiveSection extends StatelessWidget {
  const _ExpressiveSection();

  @override
  Widget build(BuildContext context) {
    final colorScheme = Theme.of(context).colorScheme;

    return _Section(
      title: '10. M3 Expressive (experimental)',
      children: [
        Text(
          'The morphing loading indicator replaces most indeterminate '
          'spinners, and MaterialShapes ships the official 35-shape library:',
          style: M3TypeScale.bodyMedium,
        ),
        const M3Gap(M3Spacings.s8),
        Row(
          children: M3GapUtils.addGaps(
            [
              SizedBox(
                width: M3Spacings.s64,
                height: M3Spacings.s64,
                child: M3ELoadingIndicator(),
              ),
              SizedBox(
                width: M3Spacings.s64,
                height: M3Spacings.s64,
                child: M3ELoadingIndicator.contained(),
              ),
              for (final shape in [
                MaterialShapes.sunny,
                MaterialShapes.cookie7Sided,
                MaterialShapes.flower,
                MaterialShapes.heart,
              ])
                CustomPaint(
                  size: const Size.square(M3Spacings.s48),
                  painter: _PolygonPainter(shape, colorScheme.primary),
                ),
            ],
            M3Spacings.s16,
          ),
        ),
      ],
    );
  }
}

/// Draws a normalized (unit-space) [RoundedPolygon] scaled to the canvas.
class _PolygonPainter extends CustomPainter {
  const _PolygonPainter(this.polygon, this.color);

  final RoundedPolygon polygon;
  final Color color;

  @override
  void paint(Canvas canvas, Size size) {
    final matrix = Matrix4.diagonal3Values(size.width, size.height, 1);
    canvas.drawPath(
      polygon.toPath().transform(matrix.storage),
      Paint()..color = color,
    );
  }

  @override
  bool shouldRepaint(_PolygonPainter oldDelegate) =>
      oldDelegate.polygon != polygon || oldDelegate.color != color;
}

// ═════════════════════════════════════════════════════════════════════════
// 11. BREAKING THE CONTRACT — M3Contract, the one greppable escape hatch
// ═════════════════════════════════════════════════════════════════════════

class _ContractSection extends StatelessWidget {
  const _ContractSection();

  @override
  Widget build(BuildContext context) {
    final colorScheme = Theme.of(context).colorScheme;

    return _Section(
      title: '11. Breaking the contract, deliberately',
      children: [
        Text(
          'A brand asset that is genuinely 18dp does not have to lie about '
          'it. M3Contract is the single sanctioned escape hatch — grep for '
          'it to audit every deviation in your codebase:',
          style: M3TypeScale.bodyMedium,
        ),
        const M3Gap(M3Spacings.s8),
        Container(
          // Off the 4dp grid, on purpose, and visibly so.
          padding: M3EdgeInsets.all(M3Contract.spacing(18)),
          decoration: M3BoxDecoration(
            color: colorScheme.tertiaryContainer,
            borderRadius: M3BorderRadius.medium,
          ),
          child: Text(
            "padding: M3EdgeInsets.all(M3Contract.spacing(18))\n"
            r"audit:   grep -rn 'M3Contract\.' lib/ | wc -l",
            style: M3TextUtils.mono(M3TypeScale.bodySmall),
          ),
        ),
      ],
    );
  }
}
30
likes
0
points
729
downloads

Publisher

unverified uploader

Weekly Downloads

A Material Design 3 contract for Flutter. Enforces M3 tokens system-wide, replacing free primitives with type-safe M3 equivalents.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter, material_color_utilities, meta, vector_math

More

Packages that depend on material_design