Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 111 additions & 74 deletions website/src/content/documentation/tutorials/creating-a-widget.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

# Building a Design System Widget

This guide walks through creating a design system button with Mix, demonstrating Specs, Stylers, variants, and state handling.
This guide walks through creating a design system button with Mix, demonstrating Specs, Stylers, variants, and state handling — using **annotations and code generation** to eliminate boilerplate.

![Button Example](./images/button-example.png)

Expand Down Expand Up @@ -33,56 +33,82 @@ This guide walks through creating a design system button with Mix, demonstrating

### Create a Button Spec

A `Spec` defines resolved visual properties. `ButtonSpec` contains specs for container, icon, and label:
A `Spec` defines resolved visual properties. With `@MixableSpec()`, the generator creates `copyWith()`, `lerp()`, `debugFillProperties()`, and `props` for you:

```dart
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:mix/mix.dart';
import 'package:mix_annotations/mix_annotations.dart';

class ButtonSpec extends Spec<ButtonSpec> {
part 'button_spec.g.dart';

@MixableSpec()
@immutable
final class ButtonSpec extends Spec<ButtonSpec>
with Diagnosticable, _$ButtonSpecMethods {
@override
final StyleSpec<FlexBoxSpec>? container;
@override
final StyleSpec<IconSpec>? icon;
@override
final StyleSpec<TextSpec>? label;

const ButtonSpec({this.container, this.icon, this.label});
}
```

That's it — no manual `copyWith`, `lerp`, or `props`. The generated mixin `_$ButtonSpecMethods` provides all of those. Run code generation with:

```bash
dart run build_runner build
```

<details>
<summary>Generated code (`button_spec.g.dart`) — key signatures</summary>

```dart
// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'button_spec.dart';

mixin _$ButtonSpecMethods on Spec<ButtonSpec>, Diagnosticable {
@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,
);
}
ButtonSpec copyWith({StyleSpec<FlexBoxSpec>? container, ...}) { ... }

@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),
);
}
ButtonSpec lerp(ButtonSpec? other, double t) { ... }

@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { ... }

@override
List<Object?> get props => [container, icon, label];
}
```

</details>

### Create a Button Styler

`ButtonStyler` provides a fluent interface for styling. It extends `Style<ButtonSpec>` and uses `WidgetStateVariantMixin` for state support:
`ButtonStyler` provides a fluent interface for styling. With `@MixableStyler()`, the generator creates setters, `merge()`, `resolve()`, `debugFillProperties()`, and `props`.

Use `@MixableField(setterType: ...)` to tell the generator the public type for each setter. Since the fields are `Prop<StyleSpec<T>>` internally but users pass Styler types, `setterType` bridges that gap:

```dart
class ButtonStyler extends Style<ButtonSpec>
with VariantStyleMixin<ButtonStyler, ButtonSpec>,
WidgetStateVariantMixin<ButtonStyler, ButtonSpec> {
part 'button_style.g.dart';

@MixableStyler()
class ButtonStyler extends MixStyler<ButtonStyler, ButtonSpec>
with _$ButtonStylerMixin {
@override
@MixableField(setterType: FlexBoxStyler)
final Prop<StyleSpec<FlexBoxSpec>>? $container;
@override
@MixableField(setterType: IconStyler)
final Prop<StyleSpec<IconSpec>>? $icon;
@override
@MixableField(setterType: TextStyler)
final Prop<StyleSpec<TextSpec>>? $label;

ButtonStyler({
Expand All @@ -96,20 +122,18 @@ class ButtonStyler extends Style<ButtonSpec>
$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));
}
const 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;

// Convenience methods
// Convenience methods (beyond the generated container/icon/label setters)
ButtonStyler backgroundColor(Color value) {
return merge(ButtonStyler(container: FlexBoxStyler().color(value)));
}
Expand All @@ -132,50 +156,54 @@ class ButtonStyler extends Style<ButtonSpec>
);
}

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),
);
ButtonStyler scale(double value) {
return merge(ButtonStyler(container: FlexBoxStyler().scale(value)));
}
}
```

The `@MixableField(setterType: FlexBoxStyler)` annotation tells the generator to produce `ButtonStyler container(FlexBoxStyler value)` instead of `ButtonStyler container(StyleSpec<FlexBoxSpec> value)`. This gives users the fluent Styler API they expect.

`MixStyler` already provides `WidgetStateVariantMixin` (for `onPressed`, `onHovered`, `onDisabled`, etc.), `VariantStyleMixin`, and `AnimationStyleMixin` — so you get state handling for free.

The generated `_$ButtonStylerMixin` handles setters, `merge()`, `resolve()`, `debugFillProperties()`, and `props`:

<details>
<summary>Generated code (`button_style.g.dart`) — key signatures</summary>

```dart
// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'button_style.dart';

mixin _$ButtonStylerMixin on Style<ButtonSpec>, Diagnosticable {
// Setters generated from @MixableField(setterType:)
ButtonStyler container(FlexBoxStyler value) { ... }
ButtonStyler icon(IconStyler value) { ... }
ButtonStyler label(TextStyler value) { ... }

// Base methods from MixStyler
ButtonStyler animate(AnimationConfig value) { ... }
ButtonStyler variants(List<VariantStyle<ButtonSpec>> value) { ... }
ButtonStyler wrap(WidgetModifierConfig value) { ... }

@override
List<Object?> get props => [$container, $icon, $label];
ButtonStyler merge(ButtonStyler? other) { ... }

@override
StyleSpec<ButtonSpec> resolve(BuildContext context) {
final container = MixOps.resolve(context, $container);
final icon = MixOps.resolve(context, $icon);
final label = MixOps.resolve(context, $label);
StyleSpec<ButtonSpec> resolve(BuildContext context) { ... }

return StyleSpec(
spec: ButtonSpec(container: container, icon: icon, label: label),
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { ... }

@override
ButtonStyler variant(Variant variant, ButtonStyler style) {
return merge(ButtonStyler(variants: [VariantStyle(variant, style)]));
}
List<Object?> get props =>
[$container, $icon, $label, $animation, $modifier, $variants];
}
```

</details>

### Define Variants

Use an enum to define button variants with their styles:
Expand Down Expand Up @@ -457,9 +485,18 @@ class ButtonExampleScreen extends StatelessWidget {

This tutorial covered:

- **ButtonSpec**: Resolved visual properties with animation support
- **ButtonStyler**: Fluent API with state handling via `WidgetStateVariantMixin`
- **`@MixableSpec`**: Generates `copyWith()`, `lerp()`, `debugFillProperties()`, and `props` — no manual boilerplate
- **`@MixableStyler`**: Generates setters, `merge()`, `resolve()`, `debugFillProperties()`, and `props` — you only write convenience methods
- **`@MixableField(setterType:)`**: Controls the public type of generated setters (e.g., accept `FlexBoxStyler` instead of `StyleSpec<FlexBoxSpec>`)
- **`MixStyler`**: Base class that provides `WidgetStateVariantMixin`, `VariantStyleMixin`, and `AnimationStyleMixin` for free
- **ButtonVariant**: Enum associating variants with styles
- **CustomButton**: Widget combining `Pressable` and `StyleBuilder`

This pattern extends to other components: cards, inputs, dialogs, etc.
### What you write vs. what's generated

| You write | Generator provides |
|---|---|
| Spec fields and constructor | `copyWith()`, `lerp()`, `debugFillProperties()`, `props` |
| Styler fields, constructors, convenience methods | Setters (via `@MixableField`), `merge()`, `resolve()`, `debugFillProperties()`, `props` |

This pattern extends to other components: cards, inputs, dialogs, etc. Add `mix_annotations` to your `dependencies` and `mix_generator` + `build_runner` to `dev_dependencies`, then run `dart run build_runner build` to generate code.