Skip to content
Blog

Widget Previews in Flutter: Using the @Preview Annotation

The Flutter Widget Previewer and @Preview annotation let you iterate on widgets without running the whole app. Setup, usage patterns, and tips.

Published on • October 15, 2026

AI Assistant

Building a widget and seeing it render shouldn’t require launching the whole app, navigating to the right screen, and faking the right state. The Flutter Widget Previewer — stable as of Flutter 3.47 — closes that loop: annotate a widget, and it renders live in a dedicated panel in your IDE or browser, isolated from the rest of your app, with hot reload and theme controls built in.

The whole workflow centers on one annotation: @Preview, from package:flutter/widget_previews.dart.

Opening the previewer

Android Studio, IntelliJ, and VS Code all start the Widget Previewer automatically on launch. Open the Flutter Widget Preview tab in the IDE sidebar to see it. From the command line, run:

flutter widget-preview start

from your project root. This launches a local server and opens the preview environment in your browser. The previewer caches project builds in a .widget_preview/ folder in your project root to speed up subsequent startups.

Annotating widgets

@Preview can be applied to:

  • Top-level functions returning a Widget or WidgetBuilder
  • Static methods within a class returning a Widget or WidgetBuilder
  • Public Widget constructors and factories with no required arguments

The minimal example:

import 'package:flutter/material.dart';
import 'package:flutter/widget_previews.dart';

@Preview(name: 'My Sample Text')
Widget mySampleText() => const Text('Hello, World!');

Each preview instance gets controls for zoom in/out, reset zoom, toggling light/dark theme, and performing a hot restart for that individual preview. A global hot restart button (bottom right of the environment) restarts everything — useful when global state like a static initializer has changed.

Customizing a preview

The annotation takes several parameters for shaping the preview context:

  • name — a descriptive label in the previewer UI
  • group — groups related previews together
  • size — artificial size constraints via a Size object
  • textScaleFactor — a custom font scale
  • wrapper — a function wrapping your widget in a surrounding tree (e.g. injecting an InheritedWidget)
  • theme — a function returning PreviewThemeData for custom theming, with sequential theme layering for matrix testing
  • brightness — the initial theme brightness
  • localizations — a localization configuration

A practical combination for a form widget:

@Preview(
  name: 'Submit Button',
  group: 'Form Controls',
  size: Size(360, 200),
  brightness: Brightness.dark,
)
Widget submitButtonPreview() => const SubmitButton();

You can also stack multiple @Preview annotations on a single function to generate several configurations at once — a common pattern for light/dark or text-scale variants:

@Preview(group: 'Brightness', name: 'Example - light', brightness: Brightness.light)
@Preview(group: 'Brightness', name: 'Example - dark', brightness: Brightness.dark)
Widget buttonPreview() => const ButtonShowcase();

Custom preview annotations

To cut boilerplate across a team, extend the Preview annotation class itself. The Flutter docs show a custom annotation that injects Material theming:

final class const MyCustomPreview({
  super.name,
  super.group,
  super.size,
  super.textScaleFactor,
  super.wrapper,
  super.brightness,
  super.localizations,
}) extends Preview {
  this : super(theme: PreviewMaterialThemeData.new);
}

final class const PreviewMaterialThemeData() extends PreviewThemeData {
  @override
  Widget apply(BuildContext context, Widget child) => Theme(
    data: .from(colorScheme: .fromSeed(seedColor: Colors.lime)),
    child: child,
  );
}

(Note the dot shorthands — .from(...) and .fromSeed(...) — Dart’s own syntax sugar for context-typed static member access.)

For previews that need computation beyond a constant context, override Preview.transform(). It’s invoked by the previewer at runtime and returns a modified Preview built through PreviewBuilder:

@override
Preview transform() {
  final originalPreview = super.transform();
  final builder = originalPreview.toBuilder();
  builder
    ..name = 'Transformed - ${originalPreview.name}'
    ..theme = _themeBuilder;
  return builder.build();
}

For creating multiple related previews programmatically, extend MultiPreview and override its previews getter (and optionally transform()).

Searching and filtering

As projects accumulate previews, navigation matters. The previewer’s search bar filters in real time against:

  • Preview name — the name parameter of @Preview
  • Group name — the group parameter
  • Containing script — the URI of the Dart file holding the preview
  • Containing package — the package name

In IDEs, there’s also a Filter previews by selected file toggle at the bottom of the environment — previews update as you move between files, so the previewer shows what you’re currently editing.

Restrictions and limitations

The previewer is built on Flutter Web, which shapes what it can and can’t do:

  • No native plugins or dart:io / dart:ffi. Widgets with transitive dependencies on these libraries load fine, but any API invocation from them throws. Web plugins may work in a browser but aren’t guaranteed inside the IDE. The docs recommend structuring code with conditional imports to isolate platform-specific code.
  • Asset paths must be package-based. When loading resources with fromAsset APIs from dart:ui, use packages/my_package_name/assets/my_image.png rather than a direct local path, so the previewer’s web environment can locate them. For web widgets, assets from your project’s web/ directory are synchronized automatically.
  • Public, constant callback arguments. All callback arguments to preview annotations must be public and constant for the previewer’s code generation to work.
  • Unconstrained widgets get constrained. Widgets without explicit constraints are clamped to roughly half the previewer’s height and width. The docs note this behavior may change, so set constraints via the size parameter when precision matters.
  • Single-project scope. The previewer currently only shows previews from one project or Pub workspace per IDE session (tracked in flutter#173550).

Workflow tips

  • Treat previews as living documentation. A @Preview(name: 'Empty state', group: 'ProductCard') next to the widget tells more than a comment.
  • Use wrapper to inject real state. A preview that exercises the widget’s error or loading branch is worth ten that only show the happy path.
  • Group aggressively. group plus the search bar is how you navigate a hundred previews without pain.
  • Use the per-preview hot restart when iterating on a single widget, and the global one only when static/global state has changed — it’s faster.

Sources

  • Flutter Widget Previewer — official guide covering setup, the @Preview annotation, customization, and restrictions (stable as of Flutter 3.47).
  • Preview class API — the annotation’s full parameter reference.