What are Stories?

A Story is a specific state or variant of a design component (or a Flutter widget). It is a way to showcase a component in different scenarios.

Stories for Components

Your design system will usually have multiple components, and each component will have multiple stories. For example, a button component might have stories for different states like Primary, Secondary, Disabled etc.

components/lib/button.dart
import 'package:flutter/material.dart';

enum ButtonState {
  primary,
  secondary,
  disabled,
}

class Button extends StatelessWidget {
  final String text;
  final ButtonState state;

  const Button({
    super.key,
    required this.text,
    required this.state,
  });

  @override
  Widget build(BuildContext context) {
    // Implementation of the button based on state
  }
}
widgetbook/lib/button.stories.dart
import 'package:flutter/material.dart';
import 'package:components/button.dart';

part 'button.stories.g.dart';

final meta = Meta<Button>();

final $Primary = _Story(
  args: _Args(
    text: StringArg('Primary'),
    state: EnumArg<ButtonState>(ButtonState.primary, values: ButtonState.values),
  ),
)

final $Secondary = _Story(
  args: _Args(
    text: StringArg('Secondary'),
    state: EnumArg<ButtonState>(ButtonState.secondary, values: ButtonState.values),
  ),
)

final $Disabled = _Story(
  args: _Args(
    text: StringArg('Disabled'),
    state: EnumArg<ButtonState>(ButtonState.disabled, values: ButtonState.values),
  ),
)

Stories for Screens

A screen is a composition of multiple components but as you move up the component hierarchy toward the screen level, you deal with more complexity. That's why it is recommended to catalog your screens in Widgetbook to be able to test them in isolation.

There are two common patterns for building screens:

  1. Pure Screens: Screens that are fully presentational and don't depend on external data or services. You can create a story for them like any other component.
  2. Contained Screens: Screens that depend on external data or services. Check out our mocking guide to know how to handle such screens.

Builder vs Setup

Every story has two hooks that control how the widget is created and wrapped:

HookSignaturePurpose
builderTWidget Function(BuildContext, TArgs)Creates the widget from args. Must return the exact widget type.
setupWidget Function(BuildContext, Widget, TArgs)Wraps the built widget. Can return any Widget.

builder is called first to construct the widget, then setup receives that widget and wraps it. When using Meta, a default builder is generated automatically. setup defaults to passing the widget through.

When to use setup

Use setup whenever your story needs a parent widget injected above it in the widget tree. This is common when your widget depends on something provided by an ancestor, e.g. a ProviderScope for Riverpod, a BlocProvider for Bloc, a custom Theme, or a mocked router.

widgetbook/lib/user_tile.stories.dart
final $Default = _Story(
  setup: (context, child, args) => ProviderScope(
    overrides: [userProvider.overrideWith((ref) => fakeUser)],
    child: child,
  ),
);

You cannot do this in builder because it must return the exact widget type (TWidget), so wrapping it in another widget would cause a type error.

This is especially useful when cataloging screens that depend on external services. See the Mocking guide for more examples of using setup with mocked dependencies.

Story Order

Stories are displayed in the navigation panel in the order they are defined in the .stories.dart file. For example, the stories below will appear as Primary, Secondary, Disabled in the sidebar and are not alphabetically sorted.

widgetbook/lib/button.stories.dart
final $Primary = _Story(...);
final $Secondary = _Story(...);
final $Disabled = _Story(...);

Component Path

You can change the navigation path of your stories by using the path parameter in Meta as follows:

If you wrap a folder name in square brackets, it will be treated as a category in the navigation path.

widgetbook/lib/button.stories.dart
final meta = Meta<Button>(
  path: 'components/button',
);