Flutter Widgetbook and AI Coding Agents: Build a Widget, Catalog It, Review It Visually
How Widgetbook turns Flutter widgets into a catalog of use cases, and how to point an AI coding agent at that catalog so it builds components, writes their stories, and self-corrects through golden and accessibility tests before a human ever looks.
Published on • October 16, 2026
AI Assistant

Ask an AI coding agent to build you a Flutter button and it will hand you a compiling StatelessWidget in a few seconds. That is the easy part. The hard part is that the button now lives inside your app, reachable only by navigating to whichever screen happens to embed it, with no way to see its disabled state, its loading state, or how it behaves on a 320pt-wide device without running the whole app.
This is exactly the problem Widgetbook solves, and it is also the missing piece that makes AI agents actually productive on UI work. Widgetbook gives an agent a structured, machine-readable surface to write its output into, and a deterministic feedback loop to check itself against.
This post covers both halves: what Widgetbook is and how you set it up, and then the workflow for using an AI coding agent to generate widgets and their catalog entries in one pass.
What Widgetbook actually is
Widgetbook is an open-source Flutter package that lets you build and preview widgets in isolation, organized into a navigable catalog. The team describes it as “a sandbox for building widgets and screens in isolation. It helps you develop hard-to-reach states and edge cases without needing to run your app.” It is the Flutter equivalent of Storybook for the web.
The mental model has three layers:
| Concept | What it is | Analogy |
|---|---|---|
| Component | A registered widget, e.g. AppButton | A page in the catalog |
| Use case | One scenario for that component, e.g. Disabled | A tab on the page |
| Knob | A live-editable parameter exposed on a use case | A control panel for that tab |
On pub.dev the package is widgetbook, published by the verified widgetbook.io publisher, with roughly 525k lifetime downloads and 800 likes. The stable line is 3.25.0, with a 4.0.0-beta.14 prerelease that introduces a new code-generated “story” API. Both matter here, and I’ll cover each.
The value proposition on the box: preview your widgets across states, devices, themes, and text scale factors; run visual regression tests; and publish the catalog for team review.
Setting up Widgetbook in three steps
The official recommendation is to create a separate Flutter app for your catalog rather than embedding it in your main app. This keeps the catalog’s dependencies (and build time) out of your production binary.
1. Create the catalog app
Inside your project directory:
flutter create widgetbook
Then in widgetbook/pubspec.yaml, add the packages. Widgetbook v3 uses annotations plus code generation:
dependencies:
flutter:
sdk: flutter
widgetbook_annotation: ^3.2.0
# import your main app as a path dependency
your_app:
path: ../
dev_dependencies:
widgetbook: ^3.10.0
widgetbook_generator: ^3.10.0
build_runner: ^2.4.13
2. Write a use case
Say your app has a CoolButton. Create widgetbook/lib/cool_button.dart:
import 'package:flutter/material.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart';
import 'package:your_app/cool_button.dart';
@UseCase(name: 'Default', type: CoolButton)
Widget buildDefaultUseCase(BuildContext context) {
return CoolButton(
label: context.knobs.string(
label: 'Label',
initialValue: 'Click me',
),
onPressed: () {},
);
}
The @UseCase annotation names the scenario and points at the widget type it exercises. Everything inside the builder is just normal Flutter code, so you can wire in whatever state you want.
3. Generate and run
dart run build_runner build -d
flutter run -d chrome -t widgetbook/lib/main.dart
build_runner scans every @UseCase and emits a main.directories.g.dart file containing the assembled catalog tree. Widgetbook then renders a navigation sidebar of all components and use cases, with a live device preview area and a knobs panel on the right.
Knobs: making states reachable without editing code
A use case with hardcoded props is a screenshot. A use case with knobs is an interactive spec. Knobs are dynamically editable parameters exposed in the Widgetbook UI panel, and they are the single feature that makes the catalog worth opening.
From the docs, the knob surface covers most Dart primitives:
| Type | Regular | Nullable | Notes |
|---|---|---|---|
bool | boolean | booleanOrNull | toggles |
int | int.input / int.slider | intOrNull.input | e.g. badge count |
double | double.input / double.slider | doubleOrNull.input | e.g. progress, opacity |
String | string | stringOrNull | labels, errors |
Duration | duration | durationOrNull | animation timing |
Color | color | — | swatch picker |
| Object | object.segmented / object.dropdown | — | enums, sealed states |
A realistic use case wires several together:
@UseCase(name: 'Interactive', type: CoolButton)
Widget buildInteractiveUseCase(BuildContext context) {
return CoolButton(
label: context.knobs.string(label: 'Label', initialValue: 'Submit'),
isLoading: context.knobs.boolean(label: 'Loading', initialValue: false),
isEnabled: context.knobs.boolean(label: 'Enabled', initialValue: true),
scale: context.knobs.double.slider(
label: 'Scale',
min: 0.8,
max: 1.4,
divisions: 6,
initialValue: 1.0,
),
);
}
You can also build custom knobs for domain types by extending KnobsBuilder and implementing valueFromQueryGroup, which is how you expose something like a RangeValues picker.
Addons: devices, themes, locales, text scale
Knobs change a widget’s inputs. Addons change the environment it renders in. The main addons are:
- DeviceFrameAddon — render inside real device bezels at actual resolutions.
- ThemeAddon — switch between your light and dark themes.
- LocalizationAddon — flip locale and
localizationsDelegatesat runtime. - TextScaleAddon — test accessibility font sizes.
runApp(
Widgetbook(
directories: directories,
addons: [
ThemeAddon(
themesBuilder: (context) => [AppTheme.light, AppTheme.dark],
),
DeviceFrameAddon(
devices: [
Device.phone,
Device.iphone13Mini,
Device.pixel7,
],
),
LocalizationAddon(
locales: [Locale('en'), Locale('de')],
localizationsDelegates: [GlobalMaterialLocalizations.delegate],
),
],
),
);
The combination is what makes the catalog useful for catching bugs early: your StatusBadge breaking in dark mode, or text overflowing at 200% scale, shows up the moment you flip a toggle instead of in a QA pass.
Widgetbook v4: a story API built for machines
Here is where it gets interesting for AI agents. Widgetbook v4 replaces annotations with a generated, typed, explicit story API. Instead of @UseCase sprinkled on builder functions, you write co-located *.stories.dart files that describe components declaratively:
// payment_request_row.stories.dart
import 'package:widgetbook/widgetbook.dart';
import 'package:banking_demo/components/payment_request_row.dart';
part 'payment_request_row.stories.g.dart';
const meta = Meta(PaymentRequestRow.new);
final $Default = _Story(
name: 'Default',
args: _Args(
name: StringArg('Alice Bergmann'),
last4: StringArg('4471'),
amount: StringArg('€50.00'),
interactive: BoolArg(true),
),
scenarios: [
_Scenario(
name: 'Enabled',
args: _Args.fixed(
name: 'Alice Bergmann',
last4: '4471',
amount: '€50.00',
interactive: true,
),
),
_Scenario(
name: 'Long name',
args: _Args.fixed(
name: 'Alexandria Featherstonehaugh-Montgomery',
last4: '4471',
amount: '€50.00',
interactive: true,
),
),
_Scenario(
name: 'Large amount',
args: _Args.fixed(
name: 'Alice Bergmann',
last4: '4471',
amount: '€12,500.00',
interactive: true,
),
),
],
);
Three properties of this format are what make it a great target for an AI agent:
Meta(Component.new)declares which widgets exist and where. A single grep tells an agent the full component inventory._Argsexposes every parameter with its type.StringArg,BoolArg,EnumArgmean the agent never has to guess a widget’s signature or read its source.scenariosare named, fixed states. “Enabled”, “Long name”, “Large amount” — these are real edge cases, not a single happy path.
It is the difference between reading an API reference and reading worked examples. An agent that can find . -name "*.stories.dart" learns your entire design system in one pass.
Widgetbook Cloud: the review layer
The open-source package is local. Widgetbook Cloud is the hosted layer on top, and it is where the AI workflow terminates. Its job is visual and accessibility regression testing on pull requests:
- Visual Pull Requests. Cloud renders every use case across every theme, breakpoint, locale, and device size, and shows a side-by-side diff against the last approved build. The visual diff, not the Dart line diff, becomes the primary review artifact.
- Accessibility regression tests. Cloud captures the semantics tree per scenario and diffs it against baseline, so a control that quietly loses its label surfaces as a regression even when the screen looks pixel-identical.
- Figma reviews. Connect a Figma design to view the Flutter widget next to the original.
- Structured, scenario-level approval. Reviewers approve or reject at the named-scenario level, which means feedback maps to something an agent can act on precisely.
The workflow on a PR: agent opens PR → CI builds the Widgetbook → Cloud generates a visual + a11y diff per scenario → human approves → branch merges with confidence. No one has to run the app and click through every state by hand.
The core idea: why agents and Widgetbook fit together
Put the pieces together and the synergy is obvious. Before AI agents, Widgetbook was a developer productivity tool. Now it doubles as an agent contract:
| Without a catalog | With a catalog |
|---|---|
| Agent writes a widget into your app | Agent writes a widget plus its story file |
| You run the app to find the disabled state | The disabled state is a named scenario |
| You eyeball the diff in a PR | Cloud shows a visual diff per scenario |
| Accessibility is an afterthought | meetsGuideline runs in the same test pass |
| Agent drifts from your design system | Story files teach the agent your system |
The rest of this post is the concrete workflow. It follows the three-phase model the Widgetbook team calls “agentic UI engineering”: prevent, self-correct, review.
Phase 0 — Prevent: give the agent your design system first
The cheapest bug is the one the agent never writes. Before generating anything, load your design system into the agent’s context.
Write a skill file
A skill is a plain text file (an AGENTS.md, a SKILL.md, or a Cursor rule) that the agent reads at the start of a session. A minimal one covers:
# Flutter Design System — Agent Rules
## Use theme tokens, never hardcoded values
Never write `Color(0x...)`, raw pixel sizes, or raw `TextStyle`.
Use `context.colors.*`, `AppSpacing.*`, and `AppTypography.*`.
## Use design-system components
Use `AppButton`, `AppCard`, `TransactionRow`, `PaymentStatusPill`.
Do NOT use raw `ElevatedButton`, `IconButton`, or `Container` for
styled surfaces — the analyzer will flag them.
## Every widget gets a story file
Co-locate `*.stories.dart` next to the widget. Write one scenario
per meaningful state: default, loading, disabled, error, empty,
long-content. Do not stop at a single "Default".
## Accessibility is mandatory
Wrap interactive groups in `Semantics` with contextual labels.
Touch targets must be at least 48x48 logical pixels.
## Discovery
List existing components:
find . -name "*.stories.dart" -not -path "*/build/*"
Back it with lint rules
A design system is only as strong as its enforcement. Custom lint rules turn “please use the design system” into an analyzer warning the agent is told to clear. A banned ElevatedButton surfaces as “Use AppButton.primary instead”, the agent reads the warning, and fixes it before the commit. Either write your own rules or reach for a tool like DCM.
The result: the agent reaches for AppButton, not IconButton, and for a token, not a hardcoded color. Prevention, not correction.
Phase 1 — The agent builds the widget and its catalog entry
With the skill loaded, you also want two MCP servers connected:
- Figma MCP — lets the agent pull real design nodes, spacing, and colors instead of eyeballing a screenshot.
- Flutter & Dart MCP — current API documentation, so the agent doesn’t hallucinate deprecated widgets.
Then the prompt is almost embarrassingly short:
Build this component: [link to Figma component]
What happens next is the part worth watching. The agent:
- Pulls the node tree, tokens, and typography from Figma over MCP.
- Scans your existing story files and finds an
AppTransactionTilethat is structurally similar to the requestedPaymentRequestRow. - Reuses that pattern and your design tokens to build the widget.
- Immediately writes the story file, with scenarios for the states it actually created.
The widget output looks like ordinary, token-driven Flutter:
import 'package:flutter/material.dart';
import '../theme/app_theme.dart';
class PaymentRequestRow extends StatelessWidget {
const PaymentRequestRow({
super.key,
required this.name,
required this.last4,
required this.amount,
});
final String name;
final String last4;
final String amount;
@override
Widget build(BuildContext context) {
final colors = context.colors;
return Container(
padding: const EdgeInsets.all(AppSpacing.xl),
decoration: BoxDecoration(
color: colors.backgroundCard,
border: Border.all(color: colors.borderSubtle),
borderRadius: AppRadius.borderRadiusXL,
),
child: Row(
children: [
_Avatar(initial: name[0]),
const SizedBox(width: AppSpacing.lg),
Expanded(child: _Info(colors: colors, name: name, last4: last4)),
const SizedBox(width: AppSpacing.lg),
_Amount(colors: colors, amount: amount),
],
),
);
}
}
And the story file that makes it catalog-testable — critically, with edge-case scenarios, not just a default:
part 'payment_request_row.stories.g.dart';
const meta = Meta(PaymentRequestRow.new);
final $Default = _Story(
name: 'Default',
scenarios: [
_Scenario(
name: 'Standard',
args: _Args.fixed(
name: 'Alice Bergmann',
last4: '4471',
amount: '€50.00',
),
),
_Scenario(
name: 'Very long name',
args: _Args.fixed(
name: 'Alexandria Featherstonehaugh-Montgomery',
last4: '4471',
amount: '€50.00',
),
),
_Scenario(
name: 'Large amount',
args: _Args.fixed(
name: 'Alice Bergmann',
last4: '4471',
amount: '€12,500.00',
),
),
_Scenario(
name: 'Overflow behavior',
run: (tester, args) async {
// interaction scenario: assert the row handles the edge case
expect(find.textContaining('...'), findsOneWidget);
},
),
],
);
This is the key behavioral shift: the story file is part of the agent’s definition of “done.” A widget without a catalog entry is an incomplete deliverable.
Phase 2 — Self-correct: the local loop in flutter test
Now the agent closes the loop on its own, without a human. Three classes of checks run in one flutter test pass, and each returns structured, machine-actionable feedback.
Check one — scenario and golden tests
Widgetbook generates a snapshot for every _Scenario. If the agent adds an interaction scenario, the test asserts real behavior:
_Scenario(
name: 'Counter increments',
run: (tester, args) async {
await tester.tap(find.byIcon(Icons.add));
await tester.pumpAndSettle();
expect(find.text('${args.initialValue + 1}'), findsOneWidget);
},
),
A failure returns the exact failed scenario and the failing assertion — not a wall of logs. The agent reads it, fixes the widget, reruns.
Check two — diff the render against the Figma source of truth
A passing golden test proves the UI is stable. It does not prove the UI matches the design. Over Figma MCP, the agent pulls the design node and diffs it against the golden file and the widget tree. This is what catches real drift: the agent learns that the reject button shipped Color(0xFFFF0000) when the design specifies colorScheme.error at #DC2626, and fixes it by resolving the token rather than nudging a pixel.
Check three — accessibility via meetsGuideline
Accessibility runs in the same test pass using Flutter’s built-in matchers. No separate service, no separate pipeline:
testWidgets('PaymentRequestRow meets a11y guidelines', (tester) async {
final handle = tester.ensureSemantics();
await tester.pumpWidget(
MaterialApp(home: Scaffold(body: PaymentRequestRow(/* ... */))),
);
await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
await expectLater(tester, meetsGuideline(iOSTapTargetGuideline));
await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
await expectLater(tester, meetsGuideline(textContrastGuideline));
handle.dispose();
});
The agent fixes itself
With a Figma diff and two accessibility failures in hand, the agent rewrites the widget without being asked:
// v1 — first attempt
Container(
decoration: const BoxDecoration(color: Color(0xFFFF0000)),
child: IconButton(
icon: const Icon(Icons.close, size: 32),
onPressed: onClose,
),
)
// v2 — after reading structured feedback
final theme = Theme.of(context);
Container(
decoration: BoxDecoration(color: theme.colorScheme.destructive),
child: Tooltip(
tooltip: (_) => const TooltipContainer(
child: Text('Reject €50 to Alice'),
),
child: IconButton(
icon: const Icon(Icons.close, size: 48), // 48x48 touch target
onPressed: onClose,
),
),
)
Both fixes came from structured feedback: the theme token from the Figma diff, the labeled 48x48 target from the accessibility check. The loop repeats until the run is green. The developer only ever sees the final result.
Phase 3 — Review: humans approve in Widgetbook Cloud
Once the local loop is green, the agent opens a PR. The reviewer works in Cloud rather than the running app:
- Visual diffs per scenario, in every mode. Every scenario the agent defined is rendered across light/dark, every breakpoint, every locale, every device size. The visual diff is the primary artifact.
- Interaction results. Did the form validate, the modal close? The reviewer trusts the test result instead of manually exercising each flow.
- Accessibility regressions. Cloud diffs the semantics tree against baseline. A node that used to announce “Accept” and now has no label shows up side by side — something a pure visual diff would miss but a screen reader user would not.
- Scenario-level feedback. A rejection maps to a named scenario, so the agent can act on it precisely in the next iteration.
The division of labour is deliberate:
| Layer | Owner | Responsibility |
|---|---|---|
| Story files & design-system context | Agent | Reads before generating |
flutter test (golden, Figma, a11y) | Agent | Self-corrects on structured feedback |
| Widgetbook Cloud | Human | Approves visual + accessibility diffs per scenario |
The agent owns coverage and correctness. The human owns judgment. The reviewer spends attention on the questions that actually need a person.
A practical adoption checklist
You do not have to do all of this at once. The Widgetbook team recommends a incremental path:
- Write a skill. Start from an example Widgetbook skill, then adapt it to your tokens, your component names, and two or three real stories from your codebase.
- Make story files the standard output. Any time the agent builds a widget, it must co-locate a
*.stories.dartwith one scenario per meaningful state. - Turn on the local loop. Run golden, Figma-MCP, and
meetsGuidelinechecks underflutter testso the agent gets deterministic feedback it can act on. - Review in Cloud. Push CI builds to Widgetbook Cloud and let humans approve visual and accessibility diffs per scenario.
For a solo developer or a small team not ready for Cloud, steps 1–3 are where most of the value lives. The catalog alone — even fully local — already stops the most common agent failure modes.
Common pitfalls
- Batch-generating use cases for the entire app at once. The generator’s directory-wide mode is still experimental and struggles with deeply nested trees. Generate per widget or per small directory.
- Stories that only cover the happy path. An agent will happily write a single
Defaultscenario if you let it. Your skill file must explicitly demand loading, empty, error, and long-content states — that is where the bugs hide. - Forgetting the catalog is a contract, not a demo. Treat the story file as part of the widget’s public surface. If a parameter changes and the story doesn’t compile, that is a signal, not noise.
- Skipping semantics in the widget itself.
meetsGuidelinecan only check what reaches the semantics tree. If the agent never wrapped the control inSemantics, the labeled-target check has nothing to inspect. - Letting the agent hardcode to match a screenshot. Connect Figma MCP so the agent diffs against real tokens, and enforce the rule with a lint so
Color(0xFFFF0000)cannot slip through.
Where this is going
Widgetbook’s own framing is worth quoting: “Strong foundations like design systems are what make agentic development succeed rather than drift.” A prompt like “build me an energy management product, make it pretty” can genuinely work — but only when the agent has a readable design system, a catalog to write into, and a deterministic test loop to check itself against.
The Flutter team is clearly leaning this way too; they launched an official MCP server so agents get current API docs instead of stale training data. The toolchain around “agent builds Flutter UI” is consolidating fast, and Widgetbook sits at the junction where generated code becomes reviewable, testable, shippable UI.
If you are already using an AI coding agent on a Flutter codebase, adding Widgetbook is one of the highest-leverage things you can do. It costs an afternoon to set up, and it converts your agent from a fast typist into one that produces components you can actually review.
Further reading
- Widgetbook documentation — quick start, knobs, addons, monorepo setup
- Widgetbook on pub.dev — install, versions, changelog
- Agentic UI engineering for Flutter — the three-phase agent workflow this post expands on
- Widgetbook Cloud — visual and accessibility regression testing on PRs
- Flutter MCP server — official docs access for coding agents
- Figma MCP server — pulling real design nodes into the agent