Layout
Safe Area
The padding that keeps an interface out from under the hardware.
import { SafeArea } from 'omaris' Examples
Basic
The same screen twice. The tinted strips are the hardware; only the right one is out from under it.
<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.
<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.
<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.
keyboardadds 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.minis the floor under an inset, not a way to add padding. - The insets as numbers, for a canvas or a measured animation → the
safeArearune. - 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=coverin the viewport meta tag, or every inset is zero.bunx omaris nativewrites it intosrc/app.html; an older project adds it by hand. - Give
minthe padding the design wanted anyway. Many phones report0for 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
SafeAreainside 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-topand--safe-bottomon 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 letstart/endfollow 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.
paddingmargin
as Defaults to 'div'
keyof HTMLElementTagNameMap The element to render. div unless the region has a better name.
children Snippet class string ref bindableDefaults to null
HTMLElement | null