Skip to content
omaris

Layout

Safe Area

The padding that keeps an interface out from under the hardware.

import { SafeArea } from 'omaris'
Learn

Examples

Basic

The same screen twice. The tinted strips are the hardware; only the right one is out from under it.

Header
Footer
A plain div
Header
Footer
SafeArea
<script lang="ts">	import { SafeArea, Text } from 'omaris';	import type { Snippet } from 'svelte';​	/* A desktop reports no insets, so the frame below pretends: `--safe-top`	   and `--safe-bottom` are exactly what the component reads, and a notch	   is nothing more than those two variables arriving with a value. */	const PHONE = '[--safe-top:44px] [--safe-bottom:34px]';</script>​{#snippet bar(label: string)}	<div class="rounded-shape-sm bg-primary-container px-3 py-2">		<Text variant="label-md" class="text-primary-container-foreground">{label}</Text>	</div>{/snippet}​{#snippet phone(caption: string, children: Snippet)}	<figure class="flex flex-col items-center gap-2">		<div			class="relative h-72 w-52 overflow-hidden rounded-shape-xl border-4 border-foreground/70 bg-surface-container-low {PHONE} max-w-full"		>			<div				class="pointer-events-none absolute inset-x-0 top-0 z-10 h-(--safe-top) bg-destructive/20"			></div>			<div				class="pointer-events-none absolute inset-x-0 bottom-0 z-10 h-(--safe-bottom) bg-destructive/20"			></div>			{@render children()}		</div>		<figcaption><Text variant="label-sm" tone="muted">{caption}</Text></figcaption>	</figure>{/snippet}​{#snippet bare()}	<div class="flex h-full flex-col justify-between p-2">		{@render bar('Header')}		{@render bar('Footer')}	</div>{/snippet}​{#snippet safe()}	<SafeArea class="flex h-full flex-col justify-between p-2">		{@render bar('Header')}		{@render bar('Footer')}	</SafeArea>{/snippet}​<div class="flex flex-wrap justify-center gap-8">	{@render phone('A plain div', bare)}	{@render phone('SafeArea', safe)}</div>

Edges

A bar reaches the glass; only its contents are held clear. One edge, and a min under it.

Inbox
Compose
<script lang="ts">	import { SafeArea, Text } from 'omaris';​	const PHONE = '[--safe-top:44px] [--safe-bottom:34px]';</script>​<div class="flex justify-center">	<div		class="relative h-72 w-52 overflow-hidden rounded-shape-xl border-4 border-foreground/70 bg-surface-container-low {PHONE} max-w-full"	>		<div			class="pointer-events-none absolute inset-x-0 top-0 z-10 h-(--safe-top) bg-destructive/20"		></div>		<div			class="pointer-events-none absolute inset-x-0 bottom-0 z-10 h-(--safe-bottom) bg-destructive/20"		></div>​		<div class="flex h-full flex-col justify-between">			<!-- The bar's own background runs under the notch, the way an app's does. -->			<header class="bg-surface-container-high">				<SafeArea edges={['top']} min={10} class="px-3 pb-2">					<Text variant="label-md">Inbox</Text>				</SafeArea>			</header>​			<footer class="bg-surface-container-high">				<SafeArea edges={['bottom']} min={10} class="px-3 pt-2">					<Text variant="label-md">Compose</Text>				</SafeArea>			</footer>		</div>	</div></div>

Measured

The same insets as numbers, from the safeArea rune. All zero on a desktop is the right answer.

top
0px
bottom
0px
start
0px
end
0px
keyboard
0px

This browser has no env(safe-area-inset-*). Nothing is in the way — a window has no notch, and a page without viewport-fit=cover is never told about one.

<script lang="ts">	import { safeArea, Text } from 'omaris';​	const rows = $derived([		['top', safeArea.top],		['bottom', safeArea.bottom],		['start', safeArea.start],		['end', safeArea.end],		['keyboard', safeArea.keyboard]	] as const);</script>​<div class="flex flex-col gap-3">	<dl class="grid w-fit grid-cols-2 gap-x-6 gap-y-1 font-mono text-sm tabular-nums">		{#each rows as [name, px] (name)}			<dt class="text-muted-foreground">{name}</dt>			<dd class="text-end">{px}px</dd>		{/each}	</dl>​	<Text variant="body-sm" tone="muted">		{safeArea.supported			? 'This browser understands env(safe-area-inset-*).'			: 'This browser has no env(safe-area-inset-*).'}		{safeArea.inset			? ' Something is in the way.'			: ' Nothing is in the way — a window has no notch, and a page without viewport-fit=cover is never told about one.'}	</Text></div>

Overridden

Overridden: mode="margin" moves a button off the edge, and the caller's own padding still wins.

Settings
<script lang="ts">	import { Button, SafeArea, Text } from 'omaris';​	const PHONE = '[--safe-top:44px] [--safe-bottom:34px]';</script>​<div class="flex justify-center">	<div		class="relative h-72 w-52 overflow-hidden rounded-shape-xl border-4 border-foreground/70 bg-surface-container-low {PHONE} max-w-full"	>		<div			class="pointer-events-none absolute inset-x-0 top-0 z-10 h-(--safe-top) bg-destructive/20"		></div>		<div			class="pointer-events-none absolute inset-x-0 bottom-0 z-10 h-(--safe-bottom) bg-destructive/20"		></div>​		<!-- `as="section"`, a floor of 2rem, and a `pt-2` the component's own		     top padding does not fight — the class prop merges last. -->		<SafeArea as="section" edges={['top']} min="2rem" class="h-full pt-2">			<Text variant="label-md" class="px-3">Settings</Text>		</SafeArea>​		<div class="absolute inset-x-0 bottom-0 flex justify-end p-3">			<SafeArea edges={['bottom']} mode="margin">				<Button size="sm">Save</Button>			</SafeArea>		</div>	</div></div>

When to use it

Use it for

  • The outermost wrapper of an app that gets the whole screen: Capacitor, a PWA opened from the home screen. One <SafeArea class="min-h-dvh"> keeps everything clear of the notch and the home indicator.
  • The contents of a bar that reaches the screen edge: a header, a bottom action row, a floating toolbar. The surface runs edge to edge and edges={['bottom']} keeps only what is inside it clear.
  • A screen in landscape, where the cutout moves to the side. edges={['start', 'end']} adds the padding portrait did not need.
  • A bottom bar that must stay above the on-screen keyboard. keyboard adds what the keyboard covers to the bottom edge.

Not for

  • A page in an ordinary browser tab → nothing. The insets are zero there.
  • Padding unrelated to the hardware → a p-* class. min is the floor under an inset, not a way to add padding.
  • The insets as numbers, for a canvas or a measured animation → the safeArea rune.
  • One edge on one element, with no minimum → pt-safe-top, pb-safe-bottom, ps-safe-start, pe-safe-end.
  • Components that already inset themselves → Top App Bar, Navigation Bar, Sheet, Dialog, Toast. Wrapping one pads it twice.

Do

  • Set viewport-fit=cover in the viewport meta tag, or every inset is zero. bunx omaris native writes it into src/app.html; an older project adds it by hand.
  • Give min the padding the design wanted anyway. Many phones report 0 for the bottom inset.
  • Name the edges as soon as something else handles one. Two nested SafeAreas on the same edge pad it twice.
  • Put the background on the bar and the SafeArea inside it, so the colour runs under the status bar.
  • Test with a notched device in the simulator, or fake it on desktop by redefining --safe-top and --safe-bottom on an ancestor.

Don't

  • Wrap every screen and the layout. Insets stack.
  • Use it to lift something off the bottom of a browser window. There is no inset there; use padding.
  • Hard-code env(safe-area-inset-top) in a component. The --safe-* variables let a preview fake them and let start/end follow the writing direction; env() only knows left and right.
  • Use mode="margin" by default. Padding keeps the element's surface against the screen edge, which is what a bar wants. Margin is for something with its own shape, like a floating button.

Quick reference

mode
  • padding (default)
  • margin

Pad with margin instead. For something that has a background of its own and should be moved off the edge rather than padded away from it.

API

SafeArea

The padding that keeps an interface out from under the hardware.

A phone does not hand a web view a rectangle. It hands it a screen with a notch or a camera hole across the top, a home indicator along the bottom, rounded corners at every one of them, and — in landscape — a bar down whichever side is currently the bottom. Anything pinned to an edge is under one of those until something pads it away, which is the single most common way a web app that looked fine in a browser looks broken once it is wrapped in Capacitor.

Wrap the app once and the whole thing is inside the usable area:

More often you want one edge, because a header or a bottom bar is supposed to reach the screen edge — it is only its contents that must not:

min is the part people forget. An inset is 0 on a phone with no home indicator, so padding that is only the inset collapses to nothing on half the devices; min is the padding you wanted anyway, and the inset takes over when it is larger.

The insets only report once the document has asked for the whole screen (viewport-fit=cover in the viewport meta — omaris native writes it). Without it every edge is 0 and this component is min and nothing else.

It is plain CSS, so it is right on the first paint and costs no JavaScript. The safeArea rune is the same numbers when you need them as numbers, and pt-safe-top / pb-safe-bottom / ps-safe-start / pe-safe-end are the same padding when a wrapper element is one element too many.

import { SafeArea } from 'omaris'
<SafeArea class="min-h-dvh">  {@render children()}</SafeArea>
<footer class="bg-surface-container">  <SafeArea edges={['bottom']} min={12}>…</SafeArea></footer>

Props

edges

Defaults to [...SAFE_AREA_EDGES]

SafeAreaEdge[]

Which edges to hold clear. Defaults to all four — name the ones you mean as soon as a parent is already handling another.

min

Defaults to 0

number | string

The padding you wanted anyway, as a floor under every inset. A number is px, a string any CSS length. Without it an edge with no hardware in the way gets no padding at all.

keyboard

Defaults to false

boolean

Add the on-screen keyboard's height to the bottom edge.

mode

Defaults to 'padding'

SafeAreaMode

Move it off the edge with margin rather than padding into it.

padding
margin
as

Defaults to 'div'

keyof HTMLElementTagNameMap

The element to render. div unless the region has a better name.

children
Snippet
class
string
ref bindable

Defaults to null

HTMLElement | null