Skip to content

[web][PWA] SafeArea Widget does not work on iOS Safari PWA #84833

Open

Description

@peerwaya

My app is running as PWA on iOS Safari but the SafeArea widget does not work as expected. see image below or
App

IMG_2BD65F959615-1

Activity

  1. darshankawar commented on Jun 18, 2021

    @darshankawar
    Member

    @peerwaya
    Can you provide flutter doctor -v and a complete minimal reproducible code sample along with index.html that has PWA script that shows this issue ?

  2. added
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Jun 18, 2021
  3. peerwaya commented on Jun 23, 2021

    @peerwaya
    Author

    here is a minimal example
    repo: https://github.com/GotokHQ/mini_test
    demo: https://dev-web-gotok.firebaseapp.com/#/

    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)
    

    IMG_60BDA6EEC15B-1

  4. removed
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Jun 23, 2021
  5. darshankawar commented on Jun 25, 2021

    @darshankawar
    Member

    Same as #84832 (comment)

    Keeping it open for further insights from team.

  6. added
    platform-webWeb applications specifically
    p: material_uimaterial_ui package in flutter/packages
    frameworkflutter/packages/flutter repository. See also f: labels.
    and removed
    in triagePresently being triaged by the triage team
    on Jun 25, 2021
  7. yjbanov commented on Jul 2, 2021

    @yjbanov
    Contributor

    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.

  8. added
    P1High-priority issues at the top of the work list
    on Jul 2, 2021
  9. 40 remaining items

  10. agostinofiscale commented on Jun 19, 2024

    @agostinofiscale

    I'm running the latest flutter version and I'm still facing this problem

  11. VladislavYakonyuk commented on Jul 14, 2024

    @VladislavYakonyuk

    Hi! Any update?

  12. cerealexx commented on Aug 19, 2024

    @cerealexx

    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 PWA
    final isPwa = kIsWeb &&
      html.window.matchMedia('(display-mode: standalone)').matches;
    // Check if it's web iOS
    final isWebiOS = kIsWeb &&
      html.window.navigator.userAgent
      .contains(RegExp(r'iPad|iPod|iPhone'));
    
    // Use Container with color instead of Padding if you need to
    return Padding(
      padding: EdgeInsets.only(bottom: isPwa && isWebiOS ? 25 : 0),
      child: YourApp(),
    );
  13. MKraust commented on Feb 20, 2025

    @MKraust

    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:

    Image

    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.

    So, I've found a solution to this:

    <!-- index.html -->
    <style>
      html {
        height: 100vh;
        width: 100vw;
      }
    </style>
    // flutter_bootstrap.js
    
    Object.defineProperty(document.documentElement, 'clientHeight', {
      get: () => window.innerHeight,
    });
    
    Object.defineProperty(document.documentElement, 'clientWidth', {
      get: () => window.innerWidth,
    });
    
    {{flutter_js}}
    {{flutter_build_config}}
    
    _flutter.loader.load();

    Now it looks like this:
    Image

    Image

    And also with SafeArea applied:

    Image Image
  14. nikzor commented on Apr 8, 2025

    @nikzor

    @MKraust
    Did you manually change flutter_bootstrap file? Could you provide full example of code please?

  15. golyshev-perfect commented on Jun 9, 2025

    @golyshev-perfect

    @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.

    Widget build(BuildContext context) {
       final device = <...>; // get device os using user-agent
       final viewPadding = MediaQuery.viewPaddingOf(context);
       
       return MediaQuery(
          data: MediaQuery.of(context).copyWith(
             viewPadding: viewPadding.copyWith(
                bottom: viewPadding.bottom + (device.isIOS ? 25 : 0),
             ),
          ),
          child: BottomNavigationBar(
             // <...>
          ),
       );
    }

    My Flutter version: 3.24.4

  16. cu-kha commented on Jun 10, 2026

    @cu-kha

    Any updates? Still having this problem in 2026. Flutter 3.44

  17. added a commit that references this issue on Jul 12, 2026
  18. diegolopezrm commented on Aug 25, 2026

    @diegolopezrm
    Contributor

    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=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.

    Happy to take feedback on the approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work listbrowser: safari-iosonly manifests in Safari on iOScustomer: castawayengineflutter/engine related. See also e: labels.platform-webWeb applications specificallyteam-webOwned by Web platform teamtriaged-webTriaged by Web platform team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions