skip to content

In a Flutter CupertinoPageScaffold, why can the first row of content hide under the CupertinoNavigationBar, and how do you fix it?

level: middleimportance: should knowfreq 30%

answer

  1. the default bar is translucent
  2. content slides behind, not below
  3. overlap reported as MediaQuery top padding
  4. ListView pads itself only when padding is null
  5. SafeArea, SliverSafeArea or an opaque bar

basics

~20 s

The default Cupertino bar background is translucent, so the scaffold lets the child draw behind it and only reports the overlap as MediaQuery top padding. Content that ignores that padding, such as a Column or a ListView with explicit padding, starts under the bar.

solid answer

~40 s

`CupertinoPageScaffold` asks the bar `shouldFullyObstruct`: only a fully opaque background pushes the child down. The theme's default `barBackgroundColor` has an alpha of `0xF0`, so the child is laid out from the top of the screen and receives a `MediaQuery` whose top padding equals the bar's height plus the status bar. A `ListView` with `padding: null` consumes that padding automatically, so it looks fine; add `padding: EdgeInsets.all(16)` and the automatic padding is gone. A `Column` or a `CustomScrollView` never applies it. Fixes: wrap the child in `SafeArea`, use `SliverSafeArea` in a `CustomScrollView`, add the media padding to your own padding, or give the bar an opaque `backgroundColor` if you do not want the frosted look.

go deeper

for a junior

Remember that the Cupertino bar is translucent by default and that SafeArea keeps content out from under it.

for a middle

Explain shouldFullyObstruct, the MediaQuery top padding the scaffold injects, and which scroll views consume it automatically.

for a senior

Diagnose overlap quickly by making the bar opaque, and prefer combining media padding with your own over hardcoded offsets.

for a principal

Decide whether the app keeps iOS translucency everywhere or standardises on opaque bars, and make shared page templates enforce it.

## The symptom In an iOS-first wine-cellar app, the cellar list looks right until someone adds 16 pixels of padding to the `ListView`, and suddenly the first bottle is hidden behind the navigation bar. Or a detail page built from a `Column` shows its heading under the bar. Nothing throws; content simply sits underneath. ## Why: translucent bars and obstruction `CupertinoPageScaffold.navigationBar` is typed `ObstructingPreferredSizeWidget`. Besides its height, such a widget answers **`shouldFullyObstruct(context)`**. `CupertinoNavigationBar` returns `true` only when its resolved background color is fully opaque. - The default comes from `CupertinoThemeData.barBackgroundColor`, a dynamic color of `0xF0F9F9F9` in light mode and `0xF01D1D1D` in dark mode: alpha `0xF0`, slightly translucent. - So by default the scaffold does **not** push the child down. The bar is stacked on top of the child, and the background blur (`enableBackgroundFilterBlur`, default `true`) shows content scrolling beneath, as on iOS. - To let the child avoid the bar, the scaffold wraps it in a `MediaQuery` whose **`padding.top`** equals the bar's preferred height plus the existing top padding (the status bar). When the bar is opaque, the scaffold instead removes the top padding from `MediaQuery` and inserts a real `Padding` above the child. | Bar background | Child laid out from | Child's `MediaQuery.padding.top` | |---|---|---| | Translucent (default) | Top of the screen | Bar height plus status bar | | Opaque (`alpha == 0xFF`) | Below the bar | 0 | ## Who honours MediaQuery padding The translucent design only works if the child reads the padding: - **`ListView`, `GridView`** (any `BoxScrollView`) apply `MediaQuery` padding along the scroll axis **only when their own `padding` is null**. Passing any `padding` replaces it. - **`CustomScrollView`** does not apply it at all; its documentation tells you to use `SliverSafeArea`. - **`Column`, `Center`, `Stack`** and most layout widgets ignore it. - **`SafeArea`** reads it and pads its child by it. ## Fixes 1. **Wrap non-scrolling content in `SafeArea`.** The simplest fix for a `Column` detail page. 2. **Keep a `ListView`'s padding null,** or combine yours with the media padding: `padding: MediaQuery.paddingOf(context) + const EdgeInsets.all(16)`. The scroll content still slides under the frosted bar, which is the iOS look. 3. **In a `CustomScrollView`, wrap slivers in `SliverSafeArea`.** 4. **Make the bar opaque** with a fully opaque `backgroundColor` when the design does not want translucency; the scaffold then pushes content down itself. ```dart import 'package:flutter/cupertino.dart'; class CellarList extends StatelessWidget { const CellarList({super.key, required this.wines}); final List<String> wines; @override Widget build(BuildContext context) { return CupertinoPageScaffold( navigationBar: const CupertinoNavigationBar(middle: Text('Cellar')), child: ListView.builder( // Keeps the bar's overlap while adding our own inset. padding: MediaQuery.paddingOf(context) + const EdgeInsets.symmetric(horizontal: 16), itemCount: wines.length, itemBuilder: (BuildContext context, int index) { return CupertinoListTile(title: Text(wines[index])); }, ), ); } } ``` ## Related behaviours worth knowing - **Keyboard:** with `resizeToAvoidBottomInset` true (the default), the scaffold pads the bottom by the keyboard inset and zeroes `viewInsets.bottom` for descendants. - **Large titles:** `CupertinoSliverNavigationBar` lives inside a `CustomScrollView` as a sliver, so overlap is handled by the scroll view layout rather than by `MediaQuery` padding. - **Material comparison:** `Scaffold` places the body below the `AppBar` unless you set `extendBodyBehindAppBar: true`, which is why developers moving from Material are caught out. ## Why Flutter does it this way The design mirrors iOS itself: navigation bars are translucent, content scrolls beneath them under a blur, and the system tells content how much of it is obscured. Flutter's version of "the system telling content" is `MediaQuery` padding, the same mechanism that describes the status bar and the home indicator. Once you see the bar as just another obstruction reported through `MediaQuery`, the rules above follow: any widget that already respects safe areas respects the bar, and any widget that ignores safe areas ignores the bar. That is also why hardcoding a top padding such as 44 pixels is wrong: it ignores the status bar height, which differs between devices, and breaks when a large title or a `bottom` widget changes the bar's height. ## How to spot it quickly Toggle a fully opaque `backgroundColor` on the bar. If the content jumps down, the page was relying on `MediaQuery` padding that some widget ignored. Then find the first widget between the scaffold and the content that does not apply it, usually a scroll view with custom padding.

  • In Flutter, why does ListView avoid a translucent CupertinoNavigationBar automatically but CustomScrollView does not?
    `BoxScrollView`, the base of `ListView` and `GridView`, pads its sliver with the ambient `MediaQuery` padding when its own `padding` is null. `CustomScrollView` takes arbitrary slivers and applies no padding itself, so you add `SliverSafeArea` around the slivers that must avoid the bar.
  • In Flutter, what changes if you give CupertinoNavigationBar an opaque backgroundColor?
    `shouldFullyObstruct` returns true, so `CupertinoPageScaffold` removes the top `MediaQuery` padding and inserts real padding above the child. Content starts below the bar, nothing scrolls beneath it, and the frosted-glass effect is lost.

saying these in an interview costs you the question

  • Assuming CupertinoPageScaffold always lays the child out below the bar
  • Believing ListView keeps its automatic safe padding after you pass padding
  • Expecting CustomScrollView to avoid the bar without SliverSafeArea
  • Fixing overlap with a hardcoded top padding of 44 pixels
  • Thinking the translucent bar is a bug rather than the iOS design