Skip to content

Instantly share code, notes, and snippets.

@mercen-lee
Created June 18, 2026 02:14
Show Gist options
  • Select an option

  • Save mercen-lee/76ec193f6ac2f2367bcae84d5acfa0d4 to your computer and use it in GitHub Desktop.

Select an option

Save mercen-lee/76ec193f6ac2f2367bcae84d5acfa0d4 to your computer and use it in GitHub Desktop.
Safearea reacting scrollview implementation for Flutter
/*
How to use ScrollSafeAreaPadding
This helper prevents ScrollView content from being hidden behind:
- top safe area: status bar, notch, Dynamic Island
- bottom safe area: iOS home indicator, Android navigation gesture area
The pattern is based on:
MediaQuery.of(context).padding.top
MediaQuery.of(context).padding.bottom
Instead of wrapping the whole ScrollView with SafeArea, this adds the safe-area
values to the scrollable content padding.
This is useful when you want:
- the ScrollView to keep occupying the full screen
- the first item to avoid the top unsafe area
- the last item to avoid the bottom unsafe area
1) ListView usage
ListView.builder(
padding: ScrollSafeAreaPadding.fromLTRB(
context,
left: 16,
top: 12,
right: 16,
bottom: 24,
includeTop: true,
includeBottom: true,
),
itemCount: items.length,
itemBuilder: (context, index) {
return Text(items[index]);
},
)
2) CustomScrollView / Sliver usage
CustomScrollView(
slivers: [
ScrollSafeAreaSliverPadding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
includeTop: true,
includeBottom: true,
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => Text(items[index]),
childCount: items.length,
),
),
),
],
)
3) Top only
ListView(
padding: ScrollSafeAreaPadding.only(
context,
top: 12,
includeTop: true,
includeBottom: false,
),
children: const [
Text('Top safe area protected'),
],
)
4) Bottom only
ListView(
padding: ScrollSafeAreaPadding.only(
context,
bottom: 24,
includeTop: false,
includeBottom: true,
),
children: const [
Text('Bottom safe area protected'),
],
)
5) Fixed bottom button
For fixed bottom buttons, use SafeArea directly.
Scaffold(
body: ...,
bottomNavigationBar: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.all(16),
child: ElevatedButton(
onPressed: () {},
child: const Text('Continue'),
),
),
),
)
Notes:
- Use MediaQuery.padding for normal safe-area scroll padding.
- Use MediaQuery.viewPadding only when you want to preserve the physical safe
area even while the keyboard is visible.
- Do not double-apply top safe area when using AppBar, SliverAppBar, or
CupertinoNavigationBar, because they may already handle it.
*/
import 'package:flutter/material.dart';
class ScrollSafeAreaPadding {
const ScrollSafeAreaPadding._();
static EdgeInsets _safeInsetsOf(
BuildContext context, {
bool maintainViewPadding = false,
}) {
final mediaQuery = MediaQuery.of(context);
return maintainViewPadding ? mediaQuery.viewPadding : mediaQuery.padding;
}
static double top(
BuildContext context, {
double top = 0,
bool maintainViewPadding = false,
}) {
final safeInsets = _safeInsetsOf(
context,
maintainViewPadding: maintainViewPadding,
);
return safeInsets.top + top;
}
static double bottom(
BuildContext context, {
double bottom = 0,
bool maintainViewPadding = false,
}) {
final safeInsets = _safeInsetsOf(
context,
maintainViewPadding: maintainViewPadding,
);
return safeInsets.bottom + bottom;
}
static EdgeInsets only(
BuildContext context, {
double left = 0,
double top = 0,
double right = 0,
double bottom = 0,
bool includeTop = false,
bool includeBottom = true,
bool maintainViewPadding = false,
}) {
final safeInsets = _safeInsetsOf(
context,
maintainViewPadding: maintainViewPadding,
);
return EdgeInsets.only(
left: left,
top: includeTop ? safeInsets.top + top : top,
right: right,
bottom: includeBottom ? safeInsets.bottom + bottom : bottom,
);
}
static EdgeInsets fromLTRB(
BuildContext context, {
double left = 0,
double top = 0,
double right = 0,
double bottom = 0,
bool includeTop = false,
bool includeBottom = true,
bool maintainViewPadding = false,
}) {
final safeInsets = _safeInsetsOf(
context,
maintainViewPadding: maintainViewPadding,
);
return EdgeInsets.fromLTRB(
left,
includeTop ? safeInsets.top + top : top,
right,
includeBottom ? safeInsets.bottom + bottom : bottom,
);
}
static EdgeInsets symmetric(
BuildContext context, {
double horizontal = 0,
double vertical = 0,
bool includeTop = false,
bool includeBottom = true,
bool maintainViewPadding = false,
}) {
final safeInsets = _safeInsetsOf(
context,
maintainViewPadding: maintainViewPadding,
);
return EdgeInsets.fromLTRB(
horizontal,
includeTop ? safeInsets.top + vertical : vertical,
horizontal,
includeBottom ? safeInsets.bottom + vertical : vertical,
);
}
static EdgeInsets all(
BuildContext context, {
double value = 0,
bool includeTop = false,
bool includeBottom = true,
bool maintainViewPadding = false,
}) {
final safeInsets = _safeInsetsOf(
context,
maintainViewPadding: maintainViewPadding,
);
return EdgeInsets.fromLTRB(
value,
includeTop ? safeInsets.top + value : value,
value,
includeBottom ? safeInsets.bottom + value : value,
);
}
static EdgeInsets add(
BuildContext context,
EdgeInsets padding, {
bool includeTop = false,
bool includeBottom = true,
bool maintainViewPadding = false,
}) {
final safeInsets = _safeInsetsOf(
context,
maintainViewPadding: maintainViewPadding,
);
return EdgeInsets.fromLTRB(
padding.left,
includeTop ? padding.top + safeInsets.top : padding.top,
padding.right,
includeBottom ? padding.bottom + safeInsets.bottom : padding.bottom,
);
}
}
/// SliverPadding version of ScrollSafeAreaPadding.
///
/// Use this inside CustomScrollView.
class ScrollSafeAreaSliverPadding extends StatelessWidget {
const ScrollSafeAreaSliverPadding({
super.key,
required this.sliver,
this.padding = EdgeInsets.zero,
this.includeTop = false,
this.includeBottom = true,
this.maintainViewPadding = false,
});
final Widget sliver;
final EdgeInsets padding;
final bool includeTop;
final bool includeBottom;
final bool maintainViewPadding;
@override
Widget build(BuildContext context) {
return SliverPadding(
padding: ScrollSafeAreaPadding.add(
context,
padding,
includeTop: includeTop,
includeBottom: includeBottom,
maintainViewPadding: maintainViewPadding,
),
sliver: sliver,
);
}
}
/// Builder helper when you want to calculate safe-area-aware padding once and
/// pass it into another widget.
class ScrollSafeAreaPaddingBuilder extends StatelessWidget {
const ScrollSafeAreaPaddingBuilder({
super.key,
required this.builder,
this.padding = EdgeInsets.zero,
this.includeTop = false,
this.includeBottom = true,
this.maintainViewPadding = false,
});
final EdgeInsets padding;
final bool includeTop;
final bool includeBottom;
final bool maintainViewPadding;
final Widget Function(BuildContext context, EdgeInsets padding) builder;
@override
Widget build(BuildContext context) {
final resolvedPadding = ScrollSafeAreaPadding.add(
context,
padding,
includeTop: includeTop,
includeBottom: includeBottom,
maintainViewPadding: maintainViewPadding,
);
return builder(context, resolvedPadding);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment