Elements

Safe area

The safe area keeps content clear of the notch or Dynamic Island, the status bar, the home indicator, a navigation header and the Mac Catalyst title bar. Any view pads itself by it with CSS’s env(): env(safe-area-inset-top), env(safe-area-inset-right), env(safe-area-inset-bottom) and env(safe-area-inset-left), in its padding or margin. They are the insets of the root view the node renders in: inside a NavPage the insets below the page’s header, inside a Dialog the dialog’s insets, and elsewhere the surface’s safe area. In a browser the page is the root, and the variables are the browser’s own.

el-safe-area.tsx
el-safe-area.tsxLive
import { mount, signal, Text, View } from "nsh";
import { safeArea, safeAreaInsets } from "nsh";
import { button, buttonText, desktop, tint, ui } from "./palette";

type Edge = "top" | "right" | "bottom" | "left";

const EDGES: Edge[] = ["top", "right", "bottom", "left"];

// `calc(env(safe-area-inset-<edge>) + 16px)` on the edges chosen, 16 px on the others.
const side = (edges: Edge[], e: Edge) => (edges.includes(e) ? `calc(env(safe-area-inset-${e}) + 16px)` : 16);
const padding = (edges: Edge[]) => ({
  paddingTop: side(edges, "top"),
  paddingRight: side(edges, "right"),
  paddingBottom: side(edges, "bottom"),
  paddingLeft: side(edges, "left"),
});

const format = (i: { top: number; right: number; bottom: number; left: number }) =>
  `top ${Math.round(i.top)} · right ${Math.round(i.right)} · bottom ${Math.round(i.bottom)} · left ${Math.round(i.left)}`;

// Called in a component: the insets where it renders (the root here, so the
// surface's safe area; inside a page or a dialog, theirs).
function Insets() {
  const insets = safeAreaInsets();
  return <Text style={{ color: ui().text, fontVariant: ["tabular-nums"] }}>{format(insets())}</Text>;
}

function App() {
  const edges = signal<Edge[]>([...EDGES]);
  const toggle = (edge: Edge) => edges.update((list) => (list.includes(edge) ? list.filter((e) => e !== edge) : EDGES.filter((e) => e === edge || list.includes(e))));
  const label = () => ({ fontSize: 12, color: ui().muted });
  return (
    // The tinted band around the card is the view's padding: the insets of
    // the edges chosen, plus 16 pt.
    <View style={{ flex: 1, backgroundColor: tint(ui().accent, 0.16), ...padding(edges()) }}>
      <View
        style={{
          flex: 1,
          width: "100%",
          maxWidth: 560,
          alignSelf: "center",
          backgroundColor: ui().page,
          borderRadius: 16,
          borderWidth: 1,
          borderStyle: "dashed",
          borderColor: ui().faint,
          padding: 20,
          gap: 14,
        }}
      >
        <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>safe area insets</Text>
        <Text style={{ color: ui().muted, lineHeight: 20 }}>
          {desktop
            ? "A desktop window or a browser page has no system UI over it: the insets are 0, so the view pads only by its 16 pt (the tinted band)."
            : "On a phone the insets are the status bar or notch on top and the home indicator below; env(safe-area-inset-*) adds them to its 16 pt. This browser frame has none, so they are 0 here."}
        </Text>

        <Text style={label()}>edges ({desktop ? "click" : "tap"} to toggle)</Text>
        <View style={{ flexDirection: "row", flexWrap: "wrap", gap: 8 }}>
          <Each of={EDGES}>
            {(edge) => {
              const on = () => edges().includes(edge());
              return (
                <View
                  role="button"
                  onPress={() => toggle(edge())}
                  {...button("secondary", { on })}
                >
                  <Text style={buttonText("secondary", { on })}>{edge()}</Text>
                </View>
              );
            }}
          </Each>
        </View>

        <View style={{ ...ui().inset, borderRadius: 12, padding: 14, gap: 6 }}>
          <Text style={label()}>edges</Text>
          <Text style={{ color: ui().text }}>{edges().length ? `[${edges().map((e) => `"${e}"`).join(", ")}]` : "[] (no inset padded)"}</Text>
          <Text style={label()}>safeArea() · the surface's insets</Text>
          <Text style={{ color: ui().text, fontVariant: ["tabular-nums"] }}>{format(safeArea())}</Text>
          <Text style={label()}>safeAreaInsets() · where it renders</Text>
          <Insets />
        </View>
      </View>
    </View>
  );
}

mount(App);
use nsh_core::prelude::*;

fn app() -> impl IntoElement {
    view()
        .flex_1()
        .bg(rgb(0xfff8c5))
        .p_safe_area()
        .child(text("Clear of the notch").text_2xl().font_weight(FontWeight::BOLD))
}

The background color fills the whole area, including the area behind the status bar, and the content starts below it. The values follow the insets: on rotation, in another page or dialog, the padding updates natively.

#Values

ValueExampleMeans
env(name)paddingTop: "env(safe-area-inset-top)"The inset.
env(name, <length>)env(safe-area-inset-top, 20px)The inset; the length stands in for an unknown name, as in CSS.
calc(env(name) + <length>)calc(env(safe-area-inset-bottom) + 16px)The inset plus a length, for content that keeps its own spacing.
calc(<length> + env(name))calc(16px + env(safe-area-inset-left))The same.

They apply to padding, paddingHorizontal, paddingVertical, paddingTop, paddingRight, paddingBottom, paddingLeft and the same margin keys. In Tailwind they are arbitrary values of p, px, py, pt, pr, pb, pl and the m utilities (spaces may be left out): pt-[env(safe-area-inset-top)], pb-[calc(env(safe-area-inset-bottom)+16px)]. The TypeScript type of these values is EnvValue. See the style reference.

In Rust the Styled methods set them: .p_safe_area(), .px_safe_area(), .py_safe_area(), .pt_safe_area(), .pr_safe_area(), .pb_safe_area(), .pl_safe_area(), the same m methods, and .pt_safe_area_plus(px(16.0)) (and pr, pb, pl, mt, mr, mb, ml) for an inset plus a length.

#Choosing the edges

Pad only the edges that need it, for example when a tab bar or a bottom bar of your own handles the bottom inset:

view()
    .flex_1()
    .pt_safe_area()
    .px_safe_area()

#Where to use it

A NavPage with a visible, opaque header already places its content below the header, so the top inset there is 0. The safe area insets are for:

  • the root of an app without a navigation stack,
  • a page with headerVisible={false} or headerTransparent,
  • the content of a full-screen Dialog,
  • bars of your own at the edges of the screen.

#safeArea and safeAreaInsets

When you need the numbers in code, read them directly. Both TSX accessors come from nsh, return an Edges object ({ top, right, bottom, left } in points) and are reactive: code that reads them runs again when they change, for example on rotation.

TSXRustReturns
safeArea()safe_area()The surface’s safe area: the parts clear of system UI (notch, home indicator, title bar).
safeAreaInsets()cx.insets()The insets where the component renders: the nearest page’s (below its header), the nearest dialog’s, or else the surface’s safe area. These are the values of the safe-area-inset-* variables. In TSX, call it in a component; it returns an accessor. A Rust component of your crate reads them with cx.insets().
use nsh_core::prelude::*;

fn bottom_bar() -> impl IntoElement {
    view()
        .flex_row()
        .p_2()
        .pb(move || if safe_area().bottom > Points::ZERO { px(4.0) } else { px(8.0) })
        .child(text("Bottom bar"))
}

For padding by the inset itself, env() is the simpler form: it updates natively, without running code.

#Platform notes

  • iOS: the insets come from UIKit’s safe area: the status bar, the notch or Dynamic Island, and the home indicator.
  • Android: NSH reports the parts of the system bars and the display cutout that overlap the NSH view. When NSH is embedded below a native toolbar, the view’s top inset is 0.
  • Mac Catalyst: the top inset is the window’s title bar.
  • Desktops (macOS, Windows, Linux, GPUI) and the web: a page’s content starts below its header, and a dialog gives its content insets of its own, as on the phones.
  • Keyboard: env(keyboard-inset-height), content kept above the software keyboard.
  • Device and environment: the safe area among the device’s other values.