Skip to main content

Safe Areas and Edge-to-Edge Screens

Hosanna exposes drawing and interaction safe-area measurements through device metrics. Use the SafeArea aggregate view to protect selected edges, or apply a screen-wide SafeAreaPresentation. Full-bleed screens can keep decorative content in the full design bounds while wrapping only controls and readable content.

The full design bounds contain drawing-safe, interaction-safe, and framework-padded content regions, with contained and full-bleed presentation choicesThe full design bounds contain drawing-safe, interaction-safe, and framework-padded content regions, with contained and full-bleed presentation choices

The Two Metrics Objects

IHosannaScreenInfo records host measurements:

interface IHosannaScreenInfo {
viewportWidth: number;
viewportHeight: number;
screenWidth: number;
screenHeight: number;
devicePixelRatio: number;
safeAreaInsets: {
top: number;
right: number;
bottom: number;
left: number;
};
interactionInsets?: {
top: number;
right: number;
bottom: number;
left: number;
};
}

safeAreaInsets protects drawing from bars, cutouts, and rounded display edges. interactionInsets additionally protects controls from system-gesture regions. Older or custom targets can omit the interaction value; SafeArea then falls back to safeAreaInsets.

IHosannaDeviceLayoutInfo turns those values into the current Hosanna layout. The most useful fields are:

FieldMeaning
designWidth, designHeightFull logical canvas used by Hosanna views.
safeAreaTop, safeAreaRight, safeAreaBottom, safeAreaLeftHost safe-area insets.
horizontalPadding, verticalPaddingAdditional framework padding for phone/tablet layout modes.
topPadding, bottomPaddingSafe-area top/bottom plus vertical framework padding.
contentX, contentY, contentWidth, contentHeightRecommended content rectangle after safe-area and layout padding.
mode, orientation, isCompact, isPortrait, isLandscapeCurrent responsive-layout classification.
revisionIncrements when an exposed layout value changes.

safeAreaInsets is also available on the layout object. topPadding and bottomPadding are not aliases for the raw insets: compact layouts add framework padding. The calculated content rectangle uses drawing-safe insets plus that framework padding; use SafeAreaInsetType.Interaction when gesture protection is required.

Choose a Presentation

decorateSafeAreaScreenViews() applies one policy to a complete screen tree:

  • SafeAreaPresentation.Contained adds an unsafe-region fill and wraps the screen in an interaction-safe SafeArea.
  • SafeAreaPresentation.FullBleed returns the authored tree unchanged, so the screen can draw edge to edge and wrap controls selectively.
return decorateSafeAreaScreenViews(views, {
presentation: SafeAreaPresentation.Contained,
unsafeAreaColor: '#000000',
});

For a full-bleed screen, place SafeArea around the content that needs protection. It keeps the full width and height supplied by its parent, clips to the available region, and offsets its layout children by the effective insets:

SafeArea([
Button({ id: 'play', text: 'Play safely' }),
])
.insetType(SafeAreaInsetType.Interaction)
.edges([
SafeAreaEdge.Top,
SafeAreaEdge.Right,
SafeAreaEdge.Bottom,
SafeAreaEdge.Left,
])
.width(layout.designWidth)
.height(layout.designHeight)

Nested SafeArea views consume only the remaining protection that an ancestor has not already supplied on each selected edge.

Read the Shared Layout

The IoC service key is deviceLayout:

import type { IHosannaDeviceLayoutInfo } from '@hs-src/hosanna-bridge-core/api';
import { inject } from '@hs-src/hosanna-bridge-core/decorators';

export class DetailsView extends BaseView<DetailsState> {
@inject('deviceLayout')
protected deviceLayout!: IHosannaDeviceLayoutInfo;

protected override getViews(): ViewStruct<ViewState>[] {
const layout = this.deviceLayout;

return [
Group([
Rectangle({
color: '#154c79',
width: layout.designWidth,
height: layout.designHeight,
}),
Group([
Label({ text: 'Draw the background to every edge' }),
Button({ text: 'Safe action' }).isInitialFocus(),
])
.translation([layout.contentX, layout.contentY])
.width(layout.contentWidth)
.height(layout.contentHeight),
])
.width(layout.designWidth)
.height(layout.designHeight),
];
}
}

The background uses the full design bounds. The content group uses the calculated content rectangle, so text, controls, and focus targets avoid the unsafe edges and the standard compact-layout padding.

Manual Layout with the Content Rectangle

Use contentX, contentY, contentWidth, and contentHeight when a custom layout needs the standard drawing-safe region plus compact-layout padding. Use SafeArea when a view should select edges, use interaction insets, or compose nested safe regions.

Updates and Object Identity

Hosanna mutates the injected deviceLayout object and its safeAreaInsets object in place. Do not hold a one-time copy of those values. Read them when building or laying out a view, and use revision when a cache needs an invalidation key.

BaseApp refreshes the metrics and invalidates the visible root when orientation, viewport, resolution, device preset, or safe-area inputs change. A view base class that needs explicit notification handling can subscribe to the shared device-metrics event:

import {
HOSANNA_DEVICE_METRICS_DID_CHANGE_NOTIFICATION,
type HosannaDeviceMetricsChangeEvent,
} from '@hs-src/hosanna-ui/lib/device-metrics-api';
import {
type INotification,
onNotification,
} from '@hs-src/hosanna-ui/lib/notification-api';

@onNotification(HOSANNA_DEVICE_METRICS_DID_CHANGE_NOTIFICATION)
protected onDeviceMetricsDidChange(
notification: INotification<HosannaDeviceMetricsChangeEvent>
): void {
this.viewManager.forceInvalidateViewState(this);
this.viewManager.invalidateViewLayout(this);
}

The event includes previous/current metrics, change reasons, and its source. Use it when a nested view or cache must react independently; ordinary visible root views already participate in BaseApp invalidation.

Fragment Computed Values

Fragment computed values expose only safeArea.insets.top, .right, .bottom, and .left. They resolve from the shared layout object. There is no safeArea.interactionInsets.* namespace.

{
"_dataMap": {
"view": {},
"data": {},
"fn": {},
"computed": [
{
"name": "safeHorizontalInset",
"fn": "max",
"args": [
"safeArea.insets.left",
"safeArea.insets.right"
]
}
]
}
}

Use the computed name as an exact field value or as a numeric constraint argument. See Fragment Computed Values.

Platform and Test Notes

  • Web reads CSS env(safe-area-inset-*) values when the browser exposes them; otherwise they are zero.
  • Native display profiles supply and normalize their host safe-area insets.
  • TV layouts normally add no compact-layout padding, but code should still use the shared metrics instead of assuming fixed values.
  • Verify portrait and landscape on at least one iOS device profile with a sensor housing or Dynamic Island and one Android cutout/gesture-navigation profile. A plain desktop browser rectangle is not sufficient native validation.

Keep decorative pixels in the full design bounds. Keep anything a person must read, tap, select, or focus inside an appropriate SafeArea or calculated content rectangle unless the design explicitly accounts for a particular edge.

See Device & Resolution for the broader viewport and design-resolution model.

Talk to us