DEV Community

Robin for Capawesome

Posted on • Originally published at capawesome.io

Capacitor Edge-to-Edge & Safe Areas: The Complete Guide

Edge-to-edge has been one of the rougher edges of building with Capacitor for the past two years. Status bars hiding the top of your header, navigation gestures obscuring buttons, an Android plugin patched into every project just to make insets work — most teams ended up with a workaround instead of a real solution.

That story changed quietly between Capacitor 8.3.0 and 8.3.2. The framework now ships proper edge-to-edge support out of the box on both platforms, and the third-party plugin most projects relied on has moved from "recommended" to "fallback for special cases." This guide is the up-to-date playbook: what the platforms expect, what Capacitor finally fixed, and the small set of CSS you actually need to write today.

What Edge-to-Edge Means, and Why It Matters Now

Edge-to-edge is the design pattern where your app's content extends all the way under the system bars — the status bar at the top, the navigation bar at the bottom, the gesture indicator on iOS. The bars are still visible, but they sit on top of your content instead of pushing it down. The visible result is a fuller, more immersive screen. The technical result is that you are now responsible for making sure nothing important — a header title, a logout button, a floating action button — ends up trapped under one of those bars.

On iOS, this has been the default since the iPhone X in 2017. The platform exposes the env(safe-area-inset-*) CSS variables for content that should stay clear of notches, the home indicator, and the status bar. Most Capacitor projects already handle this.

Android is the part that recently changed. Starting with Android 15 (API level 35), edge-to-edge is enforced for any app targeting SDK 35 or later — and once your Play Store listing requires SDK 35, every install on Android 15+ gets the new behavior. On Android 16, the previous opt-out via windowOptOutEdgeToEdgeEnforcement is gone entirely. There's no escape hatch left.

For Capacitor apps the consequence is unavoidable: your WebView now renders behind the status and navigation bars on modern Android, and your CSS needs to account for that. The good news is that, as of Capacitor 8.3.2, the framework gives you the information you need to do it cleanly.

The Old Pain Points

To appreciate what 8.3.x fixed, it helps to remember what shipping edge-to-edge with Capacitor used to look like.

The StatusBar plugin's escape hatches stopped working. For years, the common workaround was to set overlaysWebView: false and backgroundColor on the @capacitor/status-bar plugin and call it a day — the WebView would simply not extend under the status bar. That approach quietly stopped working on Android 16, because both options depend on Android's opt-out behavior, which Android 16 no longer allows.

The CSS variables returned the wrong values. Even when you tried to do things the "right way" with env(safe-area-inset-top) and friends, Android WebView versions below 140 had a bug that returned incorrect values. Your top inset would sometimes be 0, sometimes off by a few pixels — almost never right.

The de-facto workaround was a plugin. Most projects ended up installing the Android Edge-to-Edge Support plugin, which sidestepped the broken CSS variables by applying insets to the WebView itself. It worked, but it meant carrying a non-trivial third-party dependency that wouldn't be needed once the platform caught up.

The platform has now caught up.

What Capacitor 8.3.0 – 8.3.2 Fixed

Three releases over roughly six weeks closed the gap:

  • 8.3.0SystemBars now uses native safe area insets on Android. This is the foundational change. The framework reads insets from WindowInsetsCompat and injects them as parallel --safe-area-inset-* CSS custom properties. That matters because Android WebView versions below 140 still have a bug where env(safe-area-inset-*) returns the wrong values — the injected variables sidestep the bug entirely, and the recommended pattern is to combine both with a var() fallback.
  • 8.3.1 — Separate style state for the status bar and navigation bar, so styling one no longer accidentally affects the other.
  • 8.3.2 — Removed extra view padding on Android API levels ≤ 34, which had been pushing content down by an unwanted amount on older devices.

Together, these mean that on a project running Capacitor 8.3.2 or later, a single CSS rule per edge — the injected --safe-area-inset-* variable with env() as a fallback — gets you correct safe area handling on both iOS and Android, all the way down to the WebViews that still ship the env() bug. No third-party plugin, no manual JavaScript, no platform branching.

The Recommended 2026 Setup

Three core pieces, plus a couple of optional polish steps.

1. Upgrade to Capacitor 8.3.2 or Later

This is the whole prerequisite. If you're on 8.3.2+, you can skip the rest of this section. If you're on an older 8.x or on 7.x, bump it now — the upgrade is small and the payoff is large.

npm install @capacitor/core@^8.3.2 @capacitor/cli@^8.3.2
npm install @capacitor/android@^8.3.2 @capacitor/ios@^8.3.2
npx cap sync
Enter fullscreen mode Exit fullscreen mode

2. Set the Viewport Meta Tag

Both platforms require an opt-in for the WebView to extend under safe areas. Add viewport-fit=cover to your viewport meta tag in index.html:


Enter fullscreen mode Exit fullscreen mode

Without this, iOS will respect the safe areas automatically by not drawing under the notch — which is sometimes what you want, but it isn't edge-to-edge. If your goal is a full-bleed UI, the viewport-fit=cover opt-in is mandatory.

3. Use the Safe-Area CSS Variables

The web platform exposes safe-area insets via the env(safe-area-inset-*) function — four values for top, right, bottom, and left. iOS and Android WebView 140+ return correct values here; older Android WebViews don't, which is exactly why Capacitor also injects parallel --safe-area-inset-* custom properties.

The recommended pattern is to read the injected variable first and fall back to env():

.app-header {
  padding-top: var(--safe-area-inset-top, env(safe-area-inset-top, 0px));
}

.app-footer {
  padding-bottom: var(
    --safe-area-inset-bottom,
    env(safe-area-inset-bottom, 0px)
  );
}

.app-content {
  padding-left: var(--safe-area-inset-left, env(safe-area-inset-left, 0px));
  padding-right: var(--safe-area-inset-right, env(safe-area-inset-right, 0px));
}
Enter fullscreen mode Exit fullscreen mode

The trailing 0px keeps things sensible on environments where neither value is defined — desktop previews, older browsers, and the like.

If you want to combine the inset with an existing padding value, wrap the whole thing in a calc():

.app-header {
  padding-top: calc(
    var(--safe-area-inset-top, env(safe-area-inset-top, 0px)) + 1rem
  );
}
Enter fullscreen mode Exit fullscreen mode

That's the whole core of safe area handling. Everything below this is platform-specific polish.

4. Configure SystemBars (Optional)

Capacitor's built-in SystemBars API (part of @capacitor/core, no extra install) controls the styling and visibility of the system bars. The default insetsHandling: "css" is what makes the CSS variables work — leave it on.

The most common configuration is just choosing a style:

import type { CapacitorConfig } from "@capacitor/cli";

const config: CapacitorConfig = {
  plugins: {
    SystemBars: {
      style: "DARK",
    },
  },
};

export default config;
Enter fullscreen mode Exit fullscreen mode

DARK makes the icons in the status and navigation bars dark — appropriate for a light app background. LIGHT does the opposite, for a dark app. DEFAULT follows the system theme.

If you need to change the style at runtime, the API exposes setStyle() per bar:

import { SystemBars, SystemBarsStyle, SystemBarType } from "@capacitor/core";

await SystemBars.setStyle({
  bar: SystemBarType.StatusBar,
  style: SystemBarsStyle.Light,
});
Enter fullscreen mode Exit fullscreen mode

Note that the modern SystemBars API does not include setOverlaysWebView() or setBackgroundColor(). Those still exist on the legacy @capacitor/status-bar plugin for backward compatibility, but as covered earlier, they're no-ops on Android 16+. If you need a colored status bar background, the recommended approach is now an HTML element with your background color and the safe-area padding applied.

There's one related gap on Android worth knowing about: coloring the navigation bar background. In edge-to-edge mode the gesture navigation bar is transparent, so your content shows through and there's nothing to color. But devices still using the older three-button navigation render an opaque bar — and SystemBars can only set its style (dark or light icons), not its background color. To color the Android navigation bar background, the @capawesome/capacitor-navigation-bar plugin exposes a setColor(...) method:

import { NavigationBar } from "@capawesome/capacitor-navigation-bar";

await NavigationBar.setColor({ color: "#ffffff" });
Enter fullscreen mode Exit fullscreen mode

5. iOS-Specific Considerations

iOS has had safe areas for nearly a decade, so most of this works out of the box. Two settings are worth verifying.

UIViewControllerBasedStatusBarAppearance in your Info.plist controls whether each view controller can set its own status bar style or whether the value is fixed app-wide. For Capacitor apps, leave it at the default of YES so SystemBars.setStyle() works at runtime:

UIViewControllerBasedStatusBarAppearance

Enter fullscreen mode Exit fullscreen mode

UIStatusBarStyle sets the initial status bar style before your JavaScript loads. Match it to your launch screen:

UIStatusBarStyle
UIStatusBarStyleDarkContent
Enter fullscreen mode Exit fullscreen mode

That's all there is on iOS. The env(safe-area-inset-*) values have always been correct here, so the var() / env() pair from earlier resolves cleanly; the home indicator gap is handled automatically when you pad the bottom edge, and the WebView extends edge-to-edge as soon as viewport-fit=cover is set.

Ionic Framework Specifics

If you're on Ionic, almost every component already applies safe-area padding correctly. ion-header, ion-footer, ion-tab-bar, ion-toolbar, and ion-content all read the same env(safe-area-inset-*) values (re-exposed as --ion-safe-area-top, --ion-safe-area-bottom, etc.) and lay themselves out accordingly. You usually don't have to touch anything.

The exception that bites people is ion-fab. The framework only applies safe-area padding to a FAB when it can position itself relative to a header or footer. Drop a vertical="top" FAB onto a page without an ion-header and it sits at coordinate 0 — directly under the status bar. A few lines of CSS fix it:

ion-fab[vertical="top"] {
  margin-top: var(--ion-safe-area-top, 0);
}

ion-fab[vertical="bottom"] {
  margin-bottom: var(--ion-safe-area-bottom, 0);
}
Enter fullscreen mode Exit fullscreen mode

Use the attribute selectors so the rule only applies where it's needed. The 0 fallback in var() keeps the layout sensible on platforms or devices where the variable isn't set.

Bonus: Tailwind CSS with tailwindcss-safe-area

If your project uses Tailwind, hand-writing padding-top: env(safe-area-inset-top) everywhere stops feeling Tailwind-ish very quickly. The community package tailwindcss-safe-area fills that gap with a clean set of utilities.

Install it (the v4 install — for Tailwind v3, see the package README):

npm install tailwindcss-safe-area
Enter fullscreen mode Exit fullscreen mode

Then import it in your main CSS file alongside Tailwind:

@import "tailwindcss";
@import "tailwindcss-safe-area";
Enter fullscreen mode Exit fullscreen mode

Make sure your viewport meta tag still has viewport-fit=cover — the utilities rely on env() under the hood and need the WebView to be in edge-to-edge mode. One caveat: because the package uses env() directly and doesn't read the injected --safe-area-inset-* variables, users on Android WebView versions below 140 will see 0 insets. For most apps that's a rapidly shrinking minority, but if you need to cover those devices, hand-roll the var() / env() fallback on the critical elements.

The most useful classes follow the same naming pattern as Tailwind's built-in spacing:

...
...
...
Enter fullscreen mode Exit fullscreen mode

Two variants make this far more flexible than plain env():

  • pt-safe-offset-4 applies the safe-area inset plus 1rem (Tailwind's 4 spacing token), the equivalent of calc(env(safe-area-inset-top) + 1rem).
  • pb-safe-or-8 applies the safe-area inset or 2rem, whichever is larger — handy when you want a minimum bottom padding on devices without a home indicator.

There are matching utilities for margins, scroll padding, borders, height (h-screen-safe, h-dvh-safe), and inset positioning (top-safe, bottom-safe). For a Tailwind-first app, this package removes nearly all hand-written safe-area CSS.

When the Capawesome Plugin Still Fits

The Android Edge-to-Edge Support plugin is still maintained and still useful — its role has just changed. With Capacitor 8.3.2+ handling insets natively, you only need the plugin if you don't want to implement safe-area handling in CSS at all. The plugin keeps the traditional Android behavior: it applies the system bar insets directly to the WebView, so your content never goes edge-to-edge in the first place and you don't have to add a single line of env(safe-area-inset-*) CSS to your app.

That tradeoff is appealing in a couple of specific situations:

  • Legacy apps you don't want to restyle. A long-running app with hundreds of screens, none of which were written with edge-to-edge in mind, can install the plugin and keep behaving the way it always did on Android.
  • Capacitor 7 or earlier. The native fix only landed in 8.3.0. If you're still on 7, the plugin is genuinely your best option until you can upgrade.
  • You want a colored status bar background on Android. The legacy StatusBar.setBackgroundColor() no longer works on Android 16. The plugin's setBackgroundColor(), setStatusBarColor(), and setNavigationBarColor() methods still do.

For any new project, or any project where you're willing to spend an hour adding pt-safe / env(safe-area-inset-top) to a few elements, the CSS-first approach is the cleaner long-term choice — fewer dependencies, standard CSS, and consistent behavior across iOS and Android.

Conclusion

For the last two years, "doing edge-to-edge in Capacitor" meant installing a plugin and accepting some duct tape. As of Capacitor 8.3.2, it means three lines of meta tag and CSS — the standard the web platform has had for years finally works the same way on both iOS and Android. Ionic users get most of the work for free, Tailwind users get a clean utility set with tailwindcss-safe-area, and the third-party Android plugin is now a deliberate choice for the few apps that want to opt out of edge-to-edge entirely.

Got questions or war stories from your own edge-to-edge rollout? Drop a comment below.

Top comments (0)