What is the difference between Visibility and Opacity?
2 min read
Widgets & UI
Opacity(0) | Visibility(visible: false) | |
|---|---|---|
| Takes layout space | ✅ | ❌ (default) |
| Hit tests / taps | ✅ | ❌ |
| Renders / paints | ✅ (just transparent) | ❌ |
| Keeps child state | ✅ | ❌ (default) |
| Cost when hidden | Same as when visible | Near zero |
| Good for animation | ✅ (AnimatedOpacity) | ❌ (snaps in/out) |
Code in action
// Hide via Opacity — child still pays full cost
Opacity(
opacity: 0.0,
child: ExpensiveChart(), // still built + painted
);
// Hide via Visibility — child is skipped entirely
Visibility(
visible: showChart,
child: ExpensiveChart(),
);
// Hide via if — simplest of all
if (showChart) const ExpensiveChart();
// Visibility's knobs for finer control
Visibility(
visible: false,
maintainSize: true, // keep the slot of space
maintainState: true, // keep the child State alive
maintainAnimation: true, // keep animations running
maintainInteractivity: false,
replacement: const SizedBox.shrink(),
child: ExpensiveWidget(),
);
// Animated fade (the right job for Opacity)
AnimatedOpacity(
opacity: visible ? 1.0 : 0.0,
duration: const Duration(milliseconds: 250),
child: const HelloCard(),
);
When to use which
| Goal | Use |
|---|---|
| Just remove the widget from the tree | if (cond) Widget() |
| Hide but keep space reserved (layout stable) | Visibility(visible: false, maintainSize: true) |
| Hide but keep internal state (e.g., scroll position) | Visibility(... maintainState: true) |
| Fade in / fade out | AnimatedOpacity (or FadeTransition) |
| Cross-fade between two widgets | AnimatedCrossFade / AnimatedSwitcher |
| Disable interaction without changing appearance | IgnorePointer or AbsorbPointer |
Common mistakes to avoid
// ❌ Opacity(0) on expensive widgets — still paints fully
Opacity(opacity: 0, child: HeavyChart());
// ✅ Visibility or `if`
// ❌ Opacity for instant show/hide
Opacity(opacity: showChart ? 1 : 0, child: ...); // wastes work, no animation benefit
// ✅ if / Visibility
// ❌ Visibility(visible: false) when you wanted layout to stay
// Default Visibility removes from layout — set maintainSize: true to keep the gap
// ❌ Opacity wrapping a Stack of many widgets — costly
// ✅ Apply opacity at the layer level via FadeTransition / AnimatedOpacity around the smallest subtree
// ❌ Forgetting that an invisible Opacity child still receives taps
Opacity(opacity: 0, child: GestureDetector(onTap: ...)); // still tappable!
// ✅ Wrap in IgnorePointer or use Visibility
Interview follow-ups
-
Why is
Opacity(0)more expensive thanVisibility(false)? Because the child is still built, laid out, and painted — Opacity only multiplies alpha at the compositor. Visibility short-circuits the render: nothing is built or painted when hidden. -
When would you set
maintainSize: trueon a Visibility? When the surrounding layout should not jump when the widget disappears — e.g., reserving space for an error message or a loading indicator. -
AnimatedOpacityvsFadeTransition— what's the difference?AnimatedOpacityis an implicit animation — set a new opacity value and Flutter tweens.FadeTransitionis explicit — driven by anAnimation<double>you control. UseFadeTransitionwhen you need fine-grained control or coordination with other animations. -
What's the difference between
IgnorePointerandAbsorbPointer?IgnorePointerpasses touches through to widgets behind it.AbsorbPointerblocks the touches at this widget — nothing behind receives them either. Both keep the visual appearance unchanged.
How helpful was this content?
Please sign in to rate this article.