Skip to Content
Mix 2.0 is in development! You can access the Mix 1.0 docs here.
DocsTutorialsCreating a Widget

Building a Design System Widget

This guide walks through creating a design system button with Mix, demonstrating Specs, Stylers, variants, and state handling.

Button Example

Component Overview

Button Variants

Button Variants

  • Filled: Solid background color
  • Outline: Transparent background with visible border
  • Elevated: Shadow effect for raised appearance
  • Link: Looks like a clickable link, no background

Button States

Button States

  • Normal: Default state
  • Hover: Mouse over or keyboard focus
  • Pressed: Actively being pressed
  • Disabled: Non-interactive

Button Structure

  • Container: Box decoration (border radius, background color, spacing)
  • Icon (optional): Visual embellishment
  • Label: Text content

Create a Button Spec

A Spec defines resolved visual properties. ButtonSpec contains specs for container, icon, and label:

import 'package:flutter/material.dart'; import 'package:mix/mix.dart'; class ButtonSpec extends Spec<ButtonSpec> { final StyleSpec<FlexBoxSpec>? container; final StyleSpec<IconSpec>? icon; final StyleSpec<TextSpec>? label; const ButtonSpec({this.container, this.icon, this.label}); @override ButtonSpec copyWith({ StyleSpec<FlexBoxSpec>? container, StyleSpec<IconSpec>? icon, StyleSpec<TextSpec>? label, }) { return ButtonSpec( container: container ?? this.container, icon: icon ?? this.icon, label: label ?? this.label, ); } @override ButtonSpec lerp(covariant ButtonSpec? other, double t) { return ButtonSpec( container: container?.lerp(other?.container, t), icon: icon?.lerp(other?.icon, t), label: label?.lerp(other?.label, t), ); } @override List<Object?> get props => [container, icon, label]; }

Create a Button Styler

ButtonStyler provides a fluent interface for styling. It extends Style<ButtonSpec> and uses WidgetStateVariantMixin for state support:

class ButtonStyler extends Style<ButtonSpec> with WidgetStateVariantMixin<ButtonStyler, ButtonSpec> { final Prop<StyleSpec<FlexBoxSpec>>? $container; final Prop<StyleSpec<IconSpec>>? $icon; final Prop<StyleSpec<TextSpec>>? $label; ButtonStyler({ FlexBoxStyler? container, IconStyler? icon, TextStyler? label, super.animation, super.modifier, super.variants, }) : $container = Prop.maybeMix(container), $icon = Prop.maybeMix(icon), $label = Prop.maybeMix(label); // Component methods ButtonStyler container(FlexBoxStyler value) { return merge(ButtonStyler(container: value)); } ButtonStyler icon(IconStyler value) { return merge(ButtonStyler(icon: value)); } ButtonStyler label(TextStyler value) { return merge(ButtonStyler(label: value)); } // Convenience methods ButtonStyler backgroundColor(Color value) { return merge(ButtonStyler(container: FlexBoxStyler().color(value))); } ButtonStyler textColor(Color value) { return merge(ButtonStyler(label: TextStyler().color(value))); } ButtonStyler iconColor(Color value) { return merge(ButtonStyler(icon: IconStyler().color(value))); } ButtonStyler borderRadius(double value) { return merge(ButtonStyler(container: FlexBoxStyler().borderRounded(value))); } ButtonStyler padding({required double x, required double y}) { return merge( ButtonStyler(container: FlexBoxStyler().paddingX(x).paddingY(y)), ); } ButtonStyler.create({ Prop<StyleSpec<FlexBoxSpec>>? container, Prop<StyleSpec<IconSpec>>? icon, Prop<StyleSpec<TextSpec>>? label, super.animation, super.modifier, super.variants, }) : $container = container, $icon = icon, $label = label; @override ButtonStyler merge(covariant ButtonStyler? other) { return ButtonStyler.create( container: MixOps.merge($container, other?.$container), icon: MixOps.merge($icon, other?.$icon), label: MixOps.merge($label, other?.$label), animation: MixOps.mergeAnimation($animation, other?.$animation), modifier: MixOps.mergeModifier($modifier, other?.$modifier), variants: MixOps.mergeVariants($variants, other?.$variants), ); } @override List<Object?> get props => [$container, $icon, $label]; @override StyleSpec<ButtonSpec> resolve(BuildContext context) { final container = MixOps.resolve(context, $container); final icon = MixOps.resolve(context, $icon); final label = MixOps.resolve(context, $label); return StyleSpec( spec: ButtonSpec(container: container, icon: icon, label: label), ); } @override ButtonStyler variant(Variant variant, ButtonStyler style) { return merge(ButtonStyler(variants: [VariantStyle(variant, style)])); } }

Define Variants

Use an enum to define button variants with their styles:

enum ButtonVariant { filled, outlined, elevated, link; ButtonStyler get style { switch (this) { case ButtonVariant.filled: return ButtonStyler() .backgroundColor(Colors.blueAccent) .textColor(Colors.white) .iconColor(Colors.white); case ButtonVariant.outlined: return ButtonStyler() .container( FlexBoxStyler() .color(Colors.transparent) .borderAll(width: 1.5, color: Colors.blueAccent), ) .textColor(Colors.blueAccent) .iconColor(Colors.blueAccent); case ButtonVariant.elevated: return ButtonStyler() .backgroundColor(Colors.blueAccent) .textColor(Colors.white) .iconColor(Colors.white) .container( FlexBoxStyler().shadow( BoxShadowMix() .color(Colors.blueAccent.shade700) .offset(x: 0, y: 5), ), ); case ButtonVariant.link: return ButtonStyler() .container( FlexBoxStyler() .borderAll(style: BorderStyle.none) .color(Colors.transparent), ) .textColor(Colors.blueAccent) .iconColor(Colors.blueAccent); } } }

Create the Button Widget

CustomButton uses Pressable for interaction states and StyleBuilder to resolve styles:

class CustomButton extends StatelessWidget { const CustomButton({ super.key, required this.label, this.disabled = false, this.icon, required this.onPressed, this.variant = ButtonVariant.filled, this.style, }); final String label; final bool disabled; final IconData? icon; final ButtonVariant variant; final VoidCallback? onPressed; final ButtonStyler? style; @override Widget build(BuildContext context) { return Pressable( onPress: disabled ? null : onPressed, enabled: !disabled, child: StyleBuilder( style: buttonStyle(style, variant), builder: (context, spec) { return FlexBox( styleSpec: spec.container, children: [ if (icon != null) StyledIcon(icon: icon, styleSpec: spec.icon), if (label.isNotEmpty) StyledText(label, styleSpec: spec.label), ], ); }, ), ); } }

Styling Your Button

The buttonStyle function defines base styles, merges variant styles, and adds state handling:

ButtonStyler buttonStyle(ButtonStyler? style, ButtonVariant? variant) { // Base styles shared across all variants final container = FlexBoxStyler() .borderRounded(6) .paddingX(8) .paddingY(12) .spacing(8) .mainAxisAlignment(MainAxisAlignment.center) .crossAxisAlignment(CrossAxisAlignment.center) .mainAxisSize(MainAxisSize.min); final label = TextStyler().style( TextStyleMix().fontSize(16).fontWeight(FontWeight.w500), ); final icon = IconStyler().size(18); return ButtonStyler() .container(container) .label(label) .icon(icon) .merge(variant?.style) .onPressed( ButtonStyler() .container(FlexBoxStyler().scale(0.9)), ) .onDisabled( ButtonStyler() .container(FlexBoxStyler().color(Colors.blueGrey.shade100)) .label( TextStyler().style( TextStyleMix().color(Colors.blueGrey.shade700), ), ) .icon(IconStyler().color(Colors.blueGrey.shade700)), ) .merge(style); }

Button Variant Widgets

Create convenience widgets for each variant:

final class FilledButton extends CustomButton { const FilledButton({ super.key, required super.label, super.disabled = false, super.icon, required super.onPressed, super.style, }) : super(variant: ButtonVariant.filled); } final class OutlinedButton extends CustomButton { const OutlinedButton({ super.key, required super.label, super.disabled = false, super.icon, required super.onPressed, super.style, }) : super(variant: ButtonVariant.outlined); } final class ElevatedButton extends CustomButton { const ElevatedButton({ super.key, required super.label, super.disabled = false, super.icon, required super.onPressed, super.style, }) : super(variant: ButtonVariant.elevated); } final class LinkButton extends CustomButton { const LinkButton({ super.key, required super.label, super.disabled = false, super.icon, required super.onPressed, super.style, }) : super(variant: ButtonVariant.link); }

Results

Button Demo

// Main App void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return const MaterialApp( home: ButtonExampleScreen(), ); } } class ButtonExampleScreen extends StatelessWidget { const ButtonExampleScreen({super.key}); @override Widget build(BuildContext context) { final icon = Icons.favorite; return Scaffold( appBar: AppBar( title: const Text('Button Examples'), ), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ FilledButton( label: 'Filled Button', icon: icon, onPressed: () {}, ), const SizedBox(height: 10), OutlinedButton( label: 'Outlined Button', icon: icon, onPressed: () {}, ), const SizedBox(height: 10), ElevatedButton( label: 'Elevated Button', icon: icon, onPressed: () {}, ), const SizedBox(height: 10), LinkButton( label: 'Link Button', icon: icon, onPressed: () {}, ), const SizedBox(height: 20), const Text( 'Disabled State:', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 10), FilledButton( label: 'Disabled Button', icon: icon, disabled: true, onPressed: () {}, ), ], ), ), ); } }

Summary

This tutorial covered:

  • ButtonSpec: Resolved visual properties with animation support
  • ButtonStyler: Fluent API with state handling via WidgetStateVariantMixin
  • ButtonVariant: Enum associating variants with styles
  • CustomButton: Widget combining Pressable and StyleBuilder

This pattern extends to other components: cards, inputs, dialogs, etc.