Skip to main content

Command Palette

Search for a command to run...

Visibility vs Offstage vs Opacity in Flutter

Updated
6 min read
F
Thinking...

Hook: Want to hide UI while keeping state, preserving layout, or animating transparency? Flutter gives you three common tools — Visibility, Offstage, and Opacity — and each behaves differently under the hood. Pick the right one and avoid subtle bugs and performance traps.

Quick TL;DR (what each does)

  • Visibility: high-level API that can remove a child, replace it, or keep it alive with fine-grained flags (maintainState, maintainSize, etc.).

  • Offstage: keeps the child alive and laid out but not painted or hit-tested; useful for measuring or preserving state without showing UI.

  • Opacity: draws the child with fractional transparency; values between 0.0 and 1.0 can force expensive compositing.

Summary: Visibility is the swiss-army wrapper, Offstage hides but keeps life, Opacity controls visual alpha — use each for different needs.


What they actually do (rendering, layout, semantics, hit-testing)

  • Visibility

    • visible = true: builds, lays out, paints, and participates in hit testing and semantics normally.

    • visible = false (default flags): replaces child with replacement (default: SizedBox.shrink()), removing it from layout and disposing state.

    • maintainState = true: preserves State objects (child is hidden using Offstage + TickerMode), so animations and state can be kept or muted depending on other flags.

    • maintainSize = true: reserves the child's layout space even when hidden.

    • maintainSemantics / maintainInteractivity allow accessibility visibility or touch handling when hidden.

    • Implementation note: Visibility composes smaller building blocks (Offstage, TickerMode, IgnorePointer, custom RenderVisibility) depending on flags.

    One-line: Visibility gives you flexible hide/show behavior with options to preserve state, animations, size, semantics, and interactivity.

  • Offstage

    • offstage = true: the subtree is not painted, not hit-tested, and it does not take space in its parent, but Flutter still lays it out and keeps its state and render objects alive.

    • Animations continue to run (battery/CPU cost). Offstage is good for measuring or pre-building a widget before showing it.

    • Offstage children can receive focus and keyboard input even while offstage.

    One-line: Offstage hides visually but keeps the subtree alive and laid out — useful for precomputation and preserving state.

  • Opacity

    • opacity ∈ (0.0, 1.0): Flutter paints the child into an intermediate buffer and then composites it back with alpha — this triggers an offscreen buffer and can be expensive (GPU/CPU).

    • opacity == 0.0: child is not painted, but hit-testing still works unless you wrap with IgnorePointer.

    • opacity == 1.0: paints normally (no intermediate buffer).

    • For animations, prefer AnimatedOpacity or FadeTransition for efficiency.

    • Semantics: Opacity has an alwaysIncludeSemantics option if you need accessibility nodes retained.

    One-line: Opacity controls visual alpha but can force costly compositing; it doesn't automatically disable hit testing.


Hit-testing and accessibility quirks (practical gotchas)

  • Opacity(opacity: 0.0) still responds to taps. Wrap with IgnorePointer if you want it non-interactive.

  • Offstage children are not hit-tested, but they can still receive keyboard focus.

  • Visibility with maintainInteractivity = true will forward touch events to a hidden child (rare but supported).

  • Semantics: Visibility.maintainSemantics keeps widgets visible to accessibility tools even when visually hidden; Opacity requires alwaysIncludeSemantics.

One-line: Watch hit-testing and semantics — being invisible doesn't always make a widget non-interactive or inaccessible.


Performance: when things get expensive

  • Cheapest: remove the widget from the tree (no widget, no cost).

  • Visibility without maintainState: cheap — it replaces with a zero-size widget and frees resources.

  • Visibility.maintainState / Offstage: keeps the subtree alive — memory + CPU for animations or timers.

  • Opacity with fractional values: expensive because of offscreen buffers and compositing layers; avoid for large subtrees or frequent updates.

  • AnimatedOpacity vs FadeTransition: AnimatedOpacity is convenient but may still cause rebuilds each frame; FadeTransition with an AnimationController and a composited layer can be more efficient in some cases.

Profiling tip: use Flutter DevTools’ performance/CPU and raster layers view; look for unexpected composited layers or repaint boundaries.

One-line: Removing widgets = fastest; Offstage/maintainState cost memory; fractional Opacity costs compositing — profile to confirm.


Code examples (short, focused)

Visibility — remove/replace child:

Visibility(
  visible: isVisible,
  child: Text('I show when isVisible is true'),
  replacement: SizedBox.shrink(),
)

Visibility — keep state and preserve size:

Visibility(
  visible: isVisible,
  maintainState: true,
  maintainSize: true,
  child: MyStatefulWidget(), // state preserved while hidden
)

Offstage — pre-build and measure:

Offstage(
  offstage: true,
  child: MyComplexWidget(), // built and laid out, not painted
)

Opacity — visual fade (remember hit-testing):

IgnorePointer(
  ignoring: alpha == 0.0,
  child: Opacity(
    opacity: alpha, // 0.0 -> 1.0
    child: Image.asset('photo.png'),
  ),
)

AnimatedOpacity (simpler fade animation):

AnimatedOpacity(
  opacity: isVisible ? 1.0 : 0.0,
  duration: Duration(milliseconds: 300),
  child: MyWidget(),
)

One-line: Use small focused snippets — Visibility for structural control, Offstage for prebuild, Opacity for visual alpha.


Practical decision flow (choose one)

  1. Do you want to completely remove the widget and free resources?

    • Yes → remove from tree or use Visibility(visible: false) without maintainState.
  2. Do you need to keep the widget's state or pre-layout it?

    • Yes → Offstage or Visibility(maintainState: true).
  3. Do you need to hide but keep the same layout space?

    • Yes → Visibility(maintainSize: true).
  4. Do you want visual transparency or fade animations?

    • Yes → Opacity / AnimatedOpacity / FadeTransition, but profile for compositing cost.
  5. Do you need to keep semantics accessible while hidden?

    • Use maintainSemantics on Visibility or alwaysIncludeSemantics on Opacity.

One-line: Answer these questions to pick the least-surprising, most-performant option.


Accessibility & UX considerations

  • Hiding visually while exposing semantics can be useful (e.g., screen readers) — use Visibility.maintainSemantics.

  • Don’t hide essential actions behind Opacity(0.0) without ignoring pointer events; users rely on visible cues.

  • Offstage can keep focusable elements alive off-screen — be careful to not confuse keyboard users.

One-line: Match visual state and semantic state intentionally; mismatches confuse users.


Profiling checklist (how to verify you chose wisely)

  • Open Flutter DevTools → Performance and Raster tabs.

  • Look for added composited layers when you introduce Opacity.

  • Check CPU usage if you keep many Offstage/maintained subtrees (animations, timers).

  • Use the widget inspector to see whether the subtree remains in the tree (Visibility vs removed).

One-line: Profile the real device — visible performance costs often differ from expectations.


Conclusion — practical next steps

Pick the simplest option that meets your functional and UX needs. Remove widgets when you can. Use Visibility when you need fine-grained control (state, size, semantics). Use Offstage for prebuilding or measuring. Use Opacity for actual visual fading but avoid fractional opacity over big subtrees without profiling.

Try this: replace a suspicious Opacity with Visibility in your app, run DevTools, and compare FPS and composited layers. If you want, paste a snippet of your real widget tree and I’ll point out the most efficient swap.

CTA: Run a quick profile and share the relevant widget subtree — I’ll help optimize it.