Skip to content
Tech Stack29 September 2026 · 10 min read

Flutter Widget Previews in 3.47: A Practical Guide

Flutter 3.47 made the Widget Previewer stable. Here is how @Preview, wrappers and themes actually work, where previews break, and why they are not a replacement for widget tests.

Flutter Widget Previews in 3.47: A Practical Guide

Flutter finally has a stable component preview workflow, but it is useful only if previews stay independent from your app boot sequence. That one condition decides whether the Widget Previewer saves you a few minutes every hour or turns into another piece of scaffolding you maintain and then quietly abandon.

Flutter 3.47 landed on 12 August 2026 and graduated the previewer out of experimental status, along with local project caching, an abstract PreviewThemeData API for layering themes, and automatic copying of your web/ assets when you preview web widgets (What's new in Flutter 3.47). The tool matters more than its size suggests. Flutter's own numbers put the framework at over a million monthly active developers, with Apptopia measuring Flutter at nearly 30% of tracked new free iOS apps in 2024, up from around 10% in 2021 (Flutter blog, December 2024). A change to the default UI loop reaches a lot of keyboards.

If you are new to the framework itself, start with what Flutter is and where it fits before wiring previews into a project.

What does the @Preview annotation actually give you?

Five picture frames of different sizes on a cream wall, each holding the same sage leaf lit at a different colour temperature.

It renders one widget in isolation, in as many themes, sizes and text scales as you declare, without building or launching the rest of your application.

The annotation lives in package:flutter/widget_previews.dart. You can attach it to a top-level function returning a Widget or WidgetBuilder, a static method returning either, or a public constructor or factory that has no required arguments (Flutter Widget Previewer docs).

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

@Preview(name: 'PriceTag / compact', size: Size(320, 120))
Widget priceTagCompact() => const PriceTag(amountMinor: 4900, currency: 'EUR');

@Preview(name: 'PriceTag / large text', textScaleFactor: 1.8)
Widget priceTagLargeText() => const PriceTag(amountMinor: 4900, currency: 'EUR');

The parameters you will actually reach for:

ParameterWhat it does
nameLabel shown in the previewer list. This is your search key, so write it like one.
groupBuckets related previews together under one heading.
sizeApplies an artificial Size constraint instead of letting the widget fill the frame.
textScaleFactorRenders at a different font scale, which is the cheapest accessibility check you own.
wrapperRuns your widget through a function that puts a widget tree above it.
themeReturns a PreviewThemeData so one component can be rendered under several themes.
brightnessSets the starting Brightness.light or Brightness.dark.
localizationsApplies a localization configuration to the preview.

Start the previewer from the terminal with flutter widget-preview start, or let Android Studio, IntelliJ or VS Code launch it for you on project open. The first run builds a cache into a .widget_preview/ directory at your project root, which is what makes the second run feel instant.

Add that directory to .gitignore. Small, and easy to forget.

The dependency graph is the whole game

A single leaf cutting rooting in a glass of water on a bare surface, set apart from a tangled mat of roots growing through a circuit board behind it.

A preview that initializes your whole dependency graph is just a slower emulator with a smaller window.

That is not a style preference. The previewer runs in a browser-backed environment, so native plugins, dart:io and dart:ffi are unsupported, and modifying global state requires a full restart rather than a reload (Flutter Widget Previewer docs). A widget that calls Firebase.initializeApp() on the way to its first frame, or reads from path_provider, does not render. You get an error where you wanted a component.

Actually — that overstates it. Plenty of legitimate screen-level widgets sit under a provider, and refusing to preview them is throwing away most of the value. The distinction is where the dependency comes from. A widget that reads an injected value previews fine, because you can inject a fake above it. A widget that constructs its own dependency cannot, because construction happens inside the frame you are trying to render.

So the previewer applies pressure in a direction you probably already wanted to go: data in through the constructor or an inherited fake, side effects out through callbacks. If a component resists previewing, that is usually a real coupling problem rather than a tooling gap. Majid Hajian makes the adjacent argument about the loop itself in DCM's write-up on the previewer: hot reload is fast at applying a change, but it still assumes you navigate the running app to reach the state you want to look at.

You know the loop. Tweak a padding value, hot reload, tap through login, open a booking, scroll to the empty state, squint, tweak again. How many of those taps were about the widget you were editing?

Wrapping a widget without dragging the app in

A seedling in a shallow ceramic tray of soil resting on top of a large circuit board, with a few crumbs of spilled soil beside the tray.

The wrapper parameter takes a function that receives your widget and returns a tree with your widget somewhere inside it. It is where MaterialApp, Scaffold, padding, a background colour and your fake dependencies belong.

// preview_shell.dart — top-level functions, because annotation
// arguments must be const.
Widget cardShell(Widget child) => MaterialApp(
      theme: appLightTheme,
      home: Scaffold(
        backgroundColor: const Color(0xFFF4EFE6),
        body: Center(
          child: Padding(padding: const EdgeInsets.all(24), child: child),
        ),
      ),
    );

Widget cardShellWithFakeRepo(Widget child) => BookingScope(
      repository: FakeBookingRepository.loaded(),
      child: cardShell(child),
    );

Two rules keep this from sprawling. Wrappers live in one file per package, not inline next to each preview, so the shell changes in one place when your theme does. And the fake they inject is a real fake with hand-written data, not a mock framework, because the data is the thing you are looking at.

For state management specifically, the shape of your solution decides how pleasant this is. A scoped InheritedWidget or a single provider at the top of the tree wraps cleanly. A global singleton initialized in main() does not. That trade-off is worth reading about before you pick, and I went through the options in choosing Flutter state management in 2026.

Are previews a replacement for widget tests?

No. Previews show you what a widget looks like right now; widget and golden tests assert that it still looks and behaves the same way next month.

The line matters because the three tools look similar from a distance and fail in completely different ways.

Widget previewWidget testGolden test
Runs in CINoYesYes
Can fail a buildNeverOn a failed matcherOn a pixel difference
FeedbackImmediate, while you typeSeconds, when you run itSeconds, plus baseline upkeep
VerifiesWhatever you notice by lookingPresence and behaviourRendered pixels against a baseline
Best atExploring variants and statesInteraction and state logicCatching unintended visual drift

Widget tests use WidgetTester to build and interact with widgets in a test environment, finding them with Finders and asserting with matchers; pixel comparison is a separate matcher, matchesGoldenFile (Flutter widget test introduction). Nothing in a preview asserts anything. Nobody is watching it at 03:00 when a dependency bump shifts your card padding by four pixels.

The overlap is real enough that people keep asking for it to be formalized. There is an open proposal to render widget tests through the preview annotation so you can watch a test's widget tree while writing it (flutter/flutter#180149, filed 19 December 2025, P2). Until something like that ships, treat the preview as the design surface and the test file as the contract.

Where previews break on a real codebase

Six failure modes cover almost everything you will hit:

  • Firebase or any service initialized inside the widget. Move initialization to the composition root and pass the client down, or inject a fake through wrapper.
  • Plugin-backed widgets. Camera, biometrics, file pickers and anything else riding a platform channel will not render, because the environment has no platform side to talk to.
  • Relative asset paths. Assets must be referenced as package paths, such as packages/design_system/assets/logo.png, rather than the short form you use inside the owning package. This one bites hardest in a monorepo where the design system is its own package and every image suddenly resolves differently.
  • Required constructor arguments. A public constructor previews only when nothing is required, so annotate a top-level factory function instead.
  • Unconstrained widgets. A widget with no intrinsic size gets auto-constrained to roughly half the previewer's dimensions, which is rarely what you meant. Pass size explicitly.
  • Non-const callbacks. Annotation arguments are const, so onTap handlers have to be top-level or static functions rather than closures.

Half of those are the same lesson wearing different clothes. The previewer wants a widget that can be constructed from values, and it punishes widgets that reach outward for anything at build time. Performance work pushes in the same direction, which I covered in keeping Flutter lists, images and animations smooth.

A convention that survives a design system

For a component library of any size, decide these six things once and write them in the package README:

  1. Keep previews in the same file as the widget, under a // --- previews --- divider at the bottom. A separate previews/ tree drifts out of sync within a month.
  2. Name every preview ComponentName / state, because the previewer's list is flat and the name is the only thing you can scan.
  3. Set group: to the component family, not the feature, so Buttons and Cards stay together regardless of which screen uses them.
  4. Put every wrapper in one preview_shell.dart per package and export it. Two shells, plainShell and scopedShell, cover most components.
  5. Cap variants at three or four per component: default, loaded, empty or error, and one accessibility case at textScaleFactor: 1.8. Beyond that you are building a catalogue nobody reads.
  6. Add .widget_preview/ to .gitignore before the first commit that includes a preview.

The cap in step five is the one people skip, and it is the one that decides whether the previewer is still open in six months. For the rest of the toolchain around this, see my current Flutter development tool picks.

The filter that is not there yet

The previewer renders every preview in the package, with no flag to scope it to a single file or folder. That request is open as flutter/flutter#175237, filed 11 September 2025 and triaged at P2. On a package with eight components you will not care. On a design system with sixty, you will scroll past fifty-nine of them to reach the one you are editing.

Start with one component

Pick the component you have navigated to most often this month, the one that sits four taps deep behind a login and a list. Add two previews to its file: default state and error state. Run flutter widget-preview start, leave it open in a second window for a day, and count how many times you skipped launching the app.

If that count is low, your widget is probably doing its own dependency wiring. Which component would fail that test in your codebase right now?

Free resource

Free SaaS MVP Scope Template

A Notion document with the full feature checklist, MVP vs. nice-to-have table, pre-build questions, and cost signals — so you walk into any developer call knowing exactly what to ask for.

Get the template →
DL

Dusko Licanin

Full-Stack Developer · Banja Luka, Bosnia

Full-stack developer shipping SaaS MVPs, web apps, and mobile apps using AI-augmented workflows — without agency coordination overhead. Live portfolio: BookBed, Callidus, Pizzeria Bestek.

Frequently Asked Questions

What is the Flutter Widget Previewer?

It is a tool that renders individual widgets in isolation in a browser, so you can look at a component without building or launching the full app. It graduated to stable in Flutter 3.47, released on 12 August 2026, alongside local project caching in a .widget_preview/ folder, a PreviewThemeData API for layering themes, and automatic copying of your web/ assets when previewing web widgets (What's new in Flutter 3.47). You start it with flutter widget-preview start, or let Android Studio, IntelliJ or VS Code launch it when you open the project.

How do you use the @Preview annotation in Flutter?

Import package:flutter/widget_previews.dart and annotate a top-level function that returns a Widget, then open the previewer. The annotation also works on static methods returning a Widget or WidgetBuilder, and on public constructors or factories that have no required arguments (Flutter Widget Previewer docs). Its parameters are name, group, size, textScaleFactor, wrapper, theme, brightness and localizations. All annotation arguments are const, so any callback you pass has to be a top-level or static function rather than a closure written inline.

How do you open Flutter component previews in VS Code?

VS Code starts the previewer automatically when you open a Flutter project, and surfaces it in a sidebar tab; Android Studio and IntelliJ do the same. If you would rather drive it yourself, flutter widget-preview start runs a local server and opens the preview environment in your browser, which is also the path to use over SSH or in a container. The first launch pays for a build cache written to .widget_preview/ in the project root, and later launches reuse it. Only one project or Pub workspace is supported at a time in the IDEs.

Can you preview a widget that depends on Firebase or a plugin?

Only if the dependency is injected from above rather than constructed inside the widget. The previewer runs in a browser-backed environment where native plugins, dart:io and dart:ffi are unsupported, and changes to global state need a full restart (Flutter Widget Previewer docs). A widget that calls Firebase.initializeApp() or reaches a platform channel on its way to the first frame will not render. The fix is the wrapper parameter: pass a top-level function that places a scope or provider holding a hand-written fake above your widget, and preview against that.

Do Flutter widget previews replace widget tests?

No. A preview renders a widget for you to look at, and nothing in it asserts anything or runs in CI. Widget tests build and interact with widgets in a test environment using WidgetTester, finders and matchers, and golden tests compare rendered pixels against a stored baseline with matchesGoldenFile (Flutter widget test introduction). Previews shorten the loop while you are designing a component; tests are what catch the regression six weeks later. There is an open proposal to render widget tests through the preview annotation (flutter/flutter#180149), but it is unassigned at P2.