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.
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
}
}
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:
- 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.
- 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:
| Hook | Signature | Purpose |
|---|---|---|
builder | TWidget Function(BuildContext, TArgs) | Creates the widget from args. Must return the exact widget type. |
setup | Widget 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.
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.
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.
final meta = Meta<Button>(
path: 'components/button',
);

