MD3
Expressive
MATERIAL DESIGN 3 EXPRESSIVE

Safe areas

Keep MD3 layouts clear of display cutouts, status bars, and system navigation areas on iOS and Android.

Safe areas are the screen edges occupied by a display cutout, status bar, gesture area, or system navigation. They are most visible on iPhones with a notch or home indicator, but Chromium browsers can expose them on Android devices too.

The library provides one scoped set of CSS variables and Tailwind v4 utilities. This replaces scattered env(safe-area-inset-*) expressions with a single, predictable contract for application layouts and built-in edge-anchored components.

1. Import the safe-area utilities

Safe-area utilities are part of the first-party Tailwind package. Import it after Tailwind and the required library stylesheet:

css
@import "tailwindcss";
@import "@bug-on/m3-expressive/index.css";
@import "@bug-on/m3-tailwind";

2. Enable full-bleed viewport support

For a full-screen mobile app, include viewport-fit=cover in the viewport meta tag. Without it, iOS may keep content inside the safe rectangle and report zero safe-area values.

html
<meta
  name="viewport"
  content="width=device-width, initial-scale=1, viewport-fit=cover"
/>

Use the viewport policy appropriate for your product. Do not add maximum-scale=1 or user-scalable=no solely for safe areas; restricting zoom can make an app inaccessible.

3. Opt in at the document root

Add m3-safe-areas to html or body. This enables the safe-area variables only where the application is deliberately full-bleed.

Use html when your app uses portal-based components such as BottomSheetModal, Dialog, menus, or tooltips. Their content is rendered under document.body, so a class on an inner React wrapper would not be inherited by those surfaces.

tsx
// app/layout.tsx (Next.js)
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" className="m3-safe-areas">
      <body>{children}</body>
    </html>
  );
}

For a Vite-style document shell:

html
<html lang="en" class="m3-safe-areas">
  <body>
    <div id="root"></div>
  </body>
</html>

When enabled, these variables resolve to the browser-provided environment values. On browsers or devices without support, they remain 0px.

VariableEdge
--m3-safe-area-topNotch and status-bar edge
--m3-safe-area-rightTrailing landscape edge
--m3-safe-area-bottomHome indicator or system-navigation edge
--m3-safe-area-leftLeading landscape edge

Spacing and placement utilities

Use the utilities on the element that needs to avoid the edge. They work with normal Tailwind responsive and state variants.

tsx
export function MobilePage() {
  return (
    <main className="min-h-dvh bg-m3-surface pt-safe-4 pb-safe-6 px-safe-4">
      <h1 className="text-m3-headline-medium text-m3-on-surface">Inbox</h1>
    </main>
  );
}

The unsuffixed form uses only the safe inset; the numeric form adds the normal Tailwind spacing value.

NeedInset onlyInset plus spacing
Top paddingpt-safept-safe-4
Bottom paddingpb-safepb-safe-6
Horizontal paddingpx-safepx-safe-4
Vertical paddingpy-safepy-safe-4
Top marginmt-safemt-safe-4
Bottom marginmb-safemb-safe-4
Horizontal marginmx-safemx-safe-4
Vertical marginmy-safemy-safe-4
Fixed top edgetop-safetop-safe-4
Fixed bottom edgebottom-safebottom-safe-4
Physical left/rightleft-safe, right-safeleft-safe-4, right-safe-4
Logical start/endstart-safe, end-safestart-safe-4, end-safe-4

Individual physical and logical spacing utilities are also available: pl-safe, pr-safe, ps-safe, pe-safe, ml-safe, mr-safe, ms-safe, and me-safe, with the same numeric suffixes. Prefer ps-safe/pe-safe and start-safe/end-safe in bidirectional layouts.

tsx
<button
  type="button"
  className="fixed bottom-safe-4 end-safe-4 h-14 min-w-14 rounded-m3-xl bg-m3-primary px-5 text-m3-on-primary"
>
  Compose
</button>

Built-in edge components

Once m3-safe-areas is enabled at html or body, BottomDockedToolbar, BottomSheet, and BottomSheetModal consume the same variables. This keeps a docked toolbar above the home indicator and prevents an expanded bottom sheet from crossing the top unsafe area.

Do not add pb-safe to BottomDockedToolbar or manually add env(safe-area-inset-bottom) to it; that would apply the bottom inset twice. Add safe-area spacing to page content only when it must remain clear of a fixed surface.

Disable within a subtree

Use these utilities when a nested surface intentionally reaches the physical screen edge, such as a media viewer. They reset the inherited variables to zero only for that element and its descendants.

tsx
<section className="m3-no-safe-area-top">
  <img className="block w-full" alt="" src="/hero.jpg" />
</section>
UtilityEffect
m3-no-safe-areasDisable every safe-area inset
m3-no-safe-area-topDisable only the top inset
m3-no-safe-area-rightDisable only the right inset
m3-no-safe-area-bottomDisable only the bottom inset
m3-no-safe-area-leftDisable only the left inset

Troubleshooting

  • Insets are zero on an iPhone: confirm viewport-fit=cover, the m3-safe-areas class, and that the page is viewed in a browser or installed web app that exposes the CSS environment variables.
  • A modal ignores the inset: move m3-safe-areas from an app wrapper to html or body; portals do not inherit values from sibling React roots.
  • There is too much bottom space: use either a built-in edge component's automatic handling or pb-safe-* on your own element, not both on the same visual region.
  • Landscape spacing is wrong: use logical start-safe/end-safe or the left/right utilities rather than applying top/bottom padding for every orientation.

Safe areas are browser-provided geometry, not a substitute for keyboard handling. Use dvh/Visual Viewport-aware layouts for virtual-keyboard behavior.