You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[web][PWA] SafeArea Widget does not work on iOS Safari PWA #84833
My app is running as PWA on iOS Safari but the SafeArea widget does not work as expected. see image below or App
Reacted by Paolo Valerdi, ARAVINTHAN SUBRAMANIAN, Ricardo Parra, borjandev, Jordy Sinke, Chris Gabler, breucode, Elijah Yannick MIEN, Matthew Lloyd, Cardin Menkemeller and 39 more
import 'package:animations/animations.dart';
import 'package:flutter/material.dart';
import 'package:url_launcher/link.dart';
void main() {
runApp(const MyApp());
}
const url = 'https://google.com';
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _selectedIndex = 0;
final PageStorageBucket bucket = PageStorageBucket();
final List<Widget> pages = const [
Page1(),
Page2(),
];
Widget _bottomNavigationBar(int selectedIndex) => SafeArea(
child: BottomNavigationBar(
onTap: (int index) => setState(() => _selectedIndex = index),
currentIndex: selectedIndex,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.local_dining), label: 'Page1'),
BottomNavigationBarItem(
icon: Icon(Icons.assignment), label: 'Page2'),
],
),
bottom: true,
);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.amber,
body: PageTransitionSwitcher(
transitionBuilder: (
Widget child,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
fillColor: Colors.amber,
);
},
child: KeyedSubtree(
key: ValueKey(_selectedIndex),
child: pages[_selectedIndex],
),
),
bottomNavigationBar: _bottomNavigationBar(
_selectedIndex), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
class Page1 extends StatelessWidget {
const Page1({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Page 1'),
),
backgroundColor: Colors.amber,
body: const SizedBox.expand(
child: Center(
child: Text('Empty Page'),
),
),
);
}
}
class Page2 extends StatelessWidget {
const Page2({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Page 2'),
),
backgroundColor: Colors.amber,
body: Center(
child: Link(
uri: Uri.parse(url),
target: LinkTarget.blank,
builder: (context, onPressed) {
return ElevatedButton(
onPressed: onPressed, child: const Text('Button 2'));
},
),
),
);
}
}
Onyemaechis-MacBook-Pro:mini_test evenmatrix$ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel master, 2.3.0-17.0.pre.421, on Mac OS X 10.15.7 19H105 darwin-x64, locale en-US)
[!] Android toolchain - develop for Android devices (Android SDK version 30.0.3)
✗ Android license status unknown.
Run `flutter doctor --android-licenses` to accept the SDK licenses.
See https://flutter.dev/docs/get-started/install/macos#android-setup for more details.
[✓] Xcode - develop for iOS and macOS
[✓] Chrome - develop for the web
[✓] Android Studio (version 4.2)
[✓] VS Code (version 1.57.1)
[✓] Connected device (3 available)
My approach is a bit ugly but it gets things done:
import'package:universal_html/html.dart'as html;
// Check if it's an installed PWAfinal isPwa = kIsWeb &&
html.window.matchMedia('(display-mode: standalone)').matches;
// Check if it's web iOSfinal isWebiOS = kIsWeb &&
html.window.navigator.userAgent
.contains(RegExp(r'iPad|iPod|iPhone'));
// Use Container with color instead of Padding if you need toreturnPadding(
padding:EdgeInsets.only(bottom: isPwa && isWebiOS ?25:0),
child:YourApp(),
);
Reacted by deferBot, Ilia Bardinov, 菘菘 and Jinsung-viim
I've spent almost the whole day to resolve similar problem on iOS.
I had used safe-area-insets package mentioned above (thx @ipcjs) to modify viewport meta tag, but even with viewport-fit=cover set, Flutter would still use height as if it was without viewport-fit=cover. This resulted in flutter view being pushed under iOS status bar, but having blank space in the bottom of screen:
Thanks to @Oleh-Sv, I found out that instead of visualViewport.height or window.innerHeight Flutter uses document.documentElement.clientHeight on iOS devices, which leads to not taking any safe areas into account.
@cerealexx gave me an idea. Also I looked into BottomNavigationBar widget code. They are using kBottomNavigationBarHeight and MediaQuery.viewPaddingOf(context).bottom there. So you can try to increase viewPadding for iOS PWA using MediaQuery.
Widgetbuild(BuildContext context) {
final device =<...>; // get device os using user-agentfinal viewPadding =MediaQuery.viewPaddingOf(context);
returnMediaQuery(
data:MediaQuery.of(context).copyWith(
viewPadding: viewPadding.copyWith(
bottom: viewPadding.bottom + (device.isIOS ?25:0),
),
),
child:BottomNavigationBar(
// <...>
),
);
}
Worth stating the part that surprised me, because it explains why the fix
everybody in this thread has tried never worked: adding viewport-fit=cover to index.html has no effect today because the engine
deletes it. FullPageEmbeddingStrategy._applyViewportMeta() removes every <meta name="viewport"> on the page at startup and writes its own, which has no viewport-fit. WebKit then keeps the app inside the safe area, so env(safe-area-inset-*) correctly reports there is nothing to inset. The tag was
never reaching the browser.
So it's two separate bugs: the opt-in being deleted, and nothing in web_ui ever
reading env(safe-area-inset-*) (viewPadding and padding were backed by a const ViewConfiguration() that is never reassigned). Fixing either one alone
leaves the bug in place.
Once this lands you would still need both halves: viewport-fit=cover in your index.html, and SafeArea around your content. The PR adds the tag to the flutter create web template, commented out, since opting in changes your layout
to full-bleed.
The insets are deliberately gated on your page having declared viewport-fit=cover rather than passed through from env() directly. On the
simulator I measured, iOS Safari reports env(safe-area-inset-bottom) = 20px in
landscape whether or not the page is full-bleed, so forwarding it verbatim
would have moved content in every existing Flutter web app on iOS, including
apps that never asked for it.
Two things it does not cover, which is why the PR references this issue
rather than closing it:
Views embedded in a custom element still report zero. The host page positions
those, so it is the one that knows how they overlap the screen.
env(safe-area-inset-top) has unit coverage but no on-device coverage. It read
zero in all 11 configurations I measured, standalone PWAs included, and I don't
have an explanation for that. If someone here can reproduce a non-zero env(safe-area-inset-top) in mobile Safari and share the exact setup, that
would help a lot — it's the axis this issue is mostly about, and it's the
weakest part of my evidence.
Activity
darshankawar commentedon Jun 18, 2021
@peerwaya
Can you provide
flutter doctor -vand a complete minimal reproducible code sample along withindex.htmlthat has PWA script that shows this issue ?peerwaya commentedon Jun 23, 2021
here is a minimal example
repo: https://github.com/GotokHQ/mini_test
demo: https://dev-web-gotok.firebaseapp.com/#/
peerwaya commentedon Jun 23, 2021
https://github.com/GotokHQ/mini_test
darshankawar commentedon Jun 25, 2021
Same as #84832 (comment)
Keeping it open for further insights from team.
yjbanov commentedon Jul 2, 2021
I thought we already handled this, but maybe I'm wrong. We might need to do what Apple recommends here: https://webkit.org/blog/7929/designing-websites-for-iphone-x/.
Another possibly related issue: #71278.
40 remaining items
agostinofiscale commentedon Jun 19, 2024
I'm running the latest flutter version and I'm still facing this problem
VladislavYakonyuk commentedon Jul 14, 2024
Hi! Any update?
cerealexx commentedon Aug 19, 2024
My approach is a bit ugly but it gets things done:
MKraust commentedon Feb 20, 2025
I've spent almost the whole day to resolve similar problem on iOS.
I had used safe-area-insets package mentioned above (thx @ipcjs) to modify viewport meta tag, but even with
viewport-fit=coverset, Flutter would still use height as if it was withoutviewport-fit=cover. This resulted in flutter view being pushed under iOS status bar, but having blank space in the bottom of screen:Thanks to @Oleh-Sv, I found out that instead of
visualViewport.heightorwindow.innerHeightFlutter usesdocument.documentElement.clientHeighton iOS devices, which leads to not taking any safe areas into account.So, I've found a solution to this:
Now it looks like this:

And also with
SafeAreaapplied:nikzor commentedon Apr 8, 2025
@MKraust
Did you manually change flutter_bootstrap file? Could you provide full example of code please?
golyshev-perfect commentedon Jun 9, 2025
@cerealexx gave me an idea. Also I looked into
BottomNavigationBarwidget code. They are usingkBottomNavigationBarHeightandMediaQuery.viewPaddingOf(context).bottomthere. So you can try to increaseviewPaddingfor iOS PWA usingMediaQuery.My Flutter version: 3.24.4
cu-kha commentedon Jun 10, 2026
Any updates? Still having this problem in 2026. Flutter 3.44
fix(web): safe-area iOS PWA через safe-area-insets JS-либу (как в smp…
feat: initial pwa_safe_area package
fix(web): remove first-paint status-bar hairline on iOS PWA (#74)
[web] Report `env(safe-area-inset-*)` as `FlutterView.viewPadding`
diegolopezrm commentedon Aug 25, 2026
I've opened #191647 for this.
Worth stating the part that surprised me, because it explains why the fix
everybody in this thread has tried never worked: adding
viewport-fit=covertoindex.htmlhas no effect today because the enginedeletes it.
FullPageEmbeddingStrategy._applyViewportMeta()removes every<meta name="viewport">on the page at startup and writes its own, which has noviewport-fit. WebKit then keeps the app inside the safe area, soenv(safe-area-inset-*)correctly reports there is nothing to inset. The tag wasnever reaching the browser.
So it's two separate bugs: the opt-in being deleted, and nothing in
web_uieverreading
env(safe-area-inset-*)(viewPaddingandpaddingwere backed by aconst ViewConfiguration()that is never reassigned). Fixing either one aloneleaves the bug in place.
Once this lands you would still need both halves:
viewport-fit=coverin yourindex.html, andSafeAreaaround your content. The PR adds the tag to theflutter createweb template, commented out, since opting in changes your layoutto full-bleed.
The insets are deliberately gated on your page having declared
viewport-fit=coverrather than passed through fromenv()directly. On thesimulator I measured, iOS Safari reports
env(safe-area-inset-bottom) = 20pxinlandscape whether or not the page is full-bleed, so forwarding it verbatim
would have moved content in every existing Flutter web app on iOS, including
apps that never asked for it.
Two things it does not cover, which is why the PR references this issue
rather than closing it:
those, so it is the one that knows how they overlap the screen.
env(safe-area-inset-top)has unit coverage but no on-device coverage. It readzero in all 11 configurations I measured, standalone PWAs included, and I don't
have an explanation for that. If someone here can reproduce a non-zero
env(safe-area-inset-top)in mobile Safari and share the exact setup, thatwould help a lot — it's the axis this issue is mostly about, and it's the
weakest part of my evidence.
Happy to take feedback on the approach.
[web] Report `env(safe-area-inset-*)` as `FlutterView.viewPadding`
[web] Report `env(safe-area-inset-*)` as `FlutterView.viewPadding` (f…