Code Generation

Widgetbook uses Dart's build_runner to generate the boilerplate needed for Stories, Args, and Scenarios. At the core of this system is Meta, a declaration that selects a widget constructor to generate stories for. From a Meta declaration and a part directive, the generator produces type-safe args, story classes, and a component registry. This lets you focus on writing stories rather than wiring up boilerplate.

Usage

To set up code generation for a widget, create a .stories.dart file with two things: a part directive pointing to the generated file, and a Meta variable with a constructor tear-off:

dart
counter.stories.dart
import 'package:widgetbook/widgetbook.dart';
import 'package:my_app/counter.dart';

part 'counter.stories.g.dart'; 

const meta = Meta(Counter.new); 

The part directive is required since the generator writes all output into the corresponding .stories.g.dart file. Meta selects the constructor to inspect; the component widget is derived from the tear-off. Args are derived from that constructor's parameters, and the generator creates a type-safe _Args class, a _Story class with a default builder, and other generated declarations.

Meta takes a single positional argument: a constructor tear-off such as Counter.new (the unnamed constructor) or Counter.compact (a named constructor). Meta variables must be declared as const. The name of the meta variable itself doesn't matter, the generator finds it by type.

Component Customization

To customize the component as a whole, declare an optional ComponentMeta variable (at most one per stories file). All its parameters are optional:

ParameterTypeDescription
nameStringOverride the display name shown in the Widgetbook UI. Defaults to the widget class name.
pathStringSet the navigation path in the Widgetbook UI. By default, the path is derived from the file's directory structure. Wrap folder names in square brackets to create categories (e.g. [components]/counter).
docsBuilderDocsBuilderFunctionCustomize the documentation blocks shown for this component.
dart
counter.stories.dart
// Must be `final` instead of `const` since `docsBuilder` holds a closure.
final component = ComponentMeta(
  name: 'Step Counter',
  path: '[components]/counter',
  docsBuilder: (blocks) => blocks.replaceFirst<DartCommentDocBlock>(
    const TextDocBlock('A counter with increment and decrement controls.'),
  ),
);

const meta = Meta(Counter.new);

A ComponentMeta without parameters is redundant, so omit it unless you need one of the customizations above.

Defining Stories

Once the generator has run, you can define stories using the generated types. Each story variable must start with $. The variable name (without the $) becomes the story's display name in the Widgetbook UI:

dart
counter.stories.dart
final $Default = _Story(
  args: _Args(
    initialValue: IntArg(0),
  ),
);

final $StartAtTen = _Story(
  args: _Args(
    initialValue: IntArg(10),
  ),
);

Constructor Variants

Widgets often have multiple constructors that represent distinct variants, like MyButton() and MyButton.icon(). A stories file can have any number of Meta variables, one per constructor, all targeting the same widget:

dart
my_button.stories.dart
const meta = Meta(MyButton.new);

const iconMeta = Meta(MyButton.icon); 

For each Meta, the generator emits a full set of types. Types for named constructors are prefixed with the PascalCase constructor name:

dart
my_button.stories.dart
// Stories for MyButton(...)
final $Default = _Story();

// Stories for MyButton.icon(...)
final $Icon = _IconStory(); 

final $IconLarge = _IconStory(
  args: _IconArgs(size: DoubleArg(48)), 
);

All stories appear in a flat list under the same component, with the story variable's name as the display name. Factory constructors work the same way as named constructors.

Custom Args

For cases where you need a different args shape, provide Meta.argsType with a constructor tear-off of a plain Dart class. The generated args are then derived from that constructor's parameters instead of the widget's:

Since the generator can no longer infer how to construct the widget from these custom args, you must provide a builder, either per story or via a shared defaults variable.

dart
label_badge.stories.dart
import 'package:widgetbook/widgetbook.dart';
import 'package:my_app/label_badge.dart';

part 'label_badge.stories.g.dart';

const meta = Meta(LabelBadge.new, argsType: NumericBadgeInput.new); 

class NumericBadgeInput { 
  NumericBadgeInput({required this.number}); 
  final int number; 
} 

Each variant must have a distinct args type.

Providing a Builder

With a custom args type, each story needs a builder that maps the custom args to the actual widget:

dart
label_badge.stories.dart
final $Primary = _Story(
  args: _Args(
    number: IntArg(1),
  ),
  builder: (context, args) {
    return LabelBadge(
      text: args.number.toString(),
    );
  },
);

Defaults

To avoid repeating the same builder and setup in every story, define a variable of the generated _Defaults type. The generated _Story class picks it up automatically:

dart
label_badge.stories.dart
final defaults = _Defaults(
  setup: (context, child, args) {
    return Container(
      padding: const EdgeInsets.all(8),
      color: Colors.grey[300],
      child: child,
    );
  },
  builder: (context, args) {
    return LabelBadge(
      text: args.number.toString(),
    );
  },
);

The generator matches a defaults variable to its variant by the variable's type, not its name. _Defaults applies to the unnamed constructor's stories, _IconDefaults to the .icon constructor's stories, and so on. Each variant can have at most one defaults variable.

With defaults in place, stories only need to specify their args:

dart
final $Primary = _Story(
  args: _Args(
    number: IntArg(1),
  ),
);

final $Secondary = _Story(
  args: _Args(
    number: IntArg(2),
  ),
);

Individual stories can still override builder or setup when needed.

When to Use Custom Args

  • The widget constructor has parameters that don't work well as args (callbacks, controllers, complex objects).
  • You want to expose a simplified or curated set of interactive controls.
  • Multiple widget parameters should be derived from a single arg value.
  • The widget depends on a provider and you want args to mirror the provider's properties instead of the widget's constructor. This lets you mock the provider in setup using the arg values. See the Mocking guide for more details.

Generated Output

Running the generator produces two kinds of output:

  • A per-story part file (.stories.g.dart) for each story file.
  • A global component registry (components.g.dart) at the library root.

Per-Story File

For each .stories.dart file, a .stories.g.dart part file is generated containing one set of declarations per Meta variable. For named constructors, the PascalCase constructor name is inserted after the underscore (e.g. _IconStory) and after the widget name (e.g. MyButtonIconStory):

DeclarationFull NameDescription
_Args{Widget}Args (or {ArgsClass}Args for custom args)Extends StoryArgs<TWidget>. Contains one Arg field per constructor parameter (or per argsType constructor parameter when using custom args), getters for resolved values, and a .fixed() constructor. See Args.
_Story{Widget}StoryExtends Story<TWidget, Args>. When using Meta, includes a default builder that constructs the widget from args. Accepts args, setup, modes, scenarios, and builder. See Stories.
_Scenario{Widget}ScenarioTypedef for Scenario<TWidget, Args>. Used to define fixed, testable states. See Scenarios.
_Defaults{Widget}DefaultsTypedef for Defaults<TWidget, Args>. Used to share a setup and builder across the variant's stories in the file.
_ComponentComponent<TWidget, StoryArgs<TWidget>>Typedef for the component registration type. Generated once per file, as stories from all variants share one component.

Syntactic Sugar

The underscore-prefixed names (_Story, _Args, etc.) are private typedefs that point to the fully qualified generated classes. They exist so you don't have to repeat the widget name everywhere:

dart
// Without syntactic sugar, using the full generated names:
final $Primary = CounterStory(
  args: CounterArgs(
    initialValue: IntArg(0),
  ),
);

// With syntactic sugar, using the underscore aliases:
final $Primary = _Story(
  args: _Args(
    initialValue: IntArg(0),
  ),
);

Both forms are equivalent. The underscore aliases keep story files concise and consistent regardless of the widget name.

The Args Class

The generated _Args class wraps each constructor parameter in a typed Arg<T>. It provides three ways to interact with each parameter:

  • Arg fields (e.g. initialValueArg): the full Arg<T> object, useful when you need to customize the arg's behavior.
  • Value getters (e.g. initialValue): convenience getters that return the resolved value directly.
  • .fixed() constructor: creates args with constant values that won't be interactive in the Widgetbook UI. Useful for Scenarios.
dart
// Interactive args with UI controls:
final $Default = _Story(
  args: _Args(
    initialValue: IntArg(0),
  ),
);

// Fixed args without UI controls (useful for scenarios):
_Scenario(
  name: 'Start at 10',
  args: _Args.fixed(
    initialValue: 10,
  ),
);

Component Registry

A single components.g.dart file is generated at the library root. It collects every {Widget}Component variable into a list that Widgetbook uses to build the navigation tree:

dart
components.g.dart
final components = <Component>[
  CounterComponent,
  ButtonComponent,
  LabelBadgeComponent,
];

This file is regenerated whenever you add or remove story files.

Running the Generator

Run the generator from your Widgetbook package directory:

bash
dart run build_runner build

Watch Mode

For a smoother workflow, run the generator in watch mode. It re-generates automatically on every file save, so the generated types (_Story, _Args, etc.) stay up to date and your IDE autocompletion always works:

bash
dart run build_runner watch

Best Practices

  1. Use watch mode during development. Running dart run build_runner watch keeps generated code in sync with your source files automatically.
  2. Set up Meta before writing stories. Create the meta variable and part directive first, run the generator once, and then start writing stories. This ensures _Story, _Args, and other generated types are available for autocompletion in your IDE.
  3. Prefer widget-derived args over custom args. Without argsType, you get an auto-generated builder, so you don't need to manually map args to widget parameters. Only provide argsType when the widget's constructor doesn't fit your needs.
  4. Prefix story variables with $. The generator requires this prefix (e.g. $Default, $Primary) and strips the $ to derive the display name shown in the Widgetbook UI.