One Logo Pool Ball
wwwwwwwwwwwwwwwwwww

Adaptive Layout

Size classes, foldable hinges, reserved regions, and window segments

Phones, tablets, split screens, and foldables give an app windows of very different shapes. These hooks tell a layout what it is working with, in the terms each platform uses:

All of them are named imports from one, and the readings are already current on the first render.

Usage

import { useSizeClass } from 'one'
function Inbox() {
const { horizontal } = useSizeClass()
return horizontal === 'regular' ? <SplitInbox /> : <StackedInbox />
}

useSizeClass re-renders when the window changes class, for example when an iPad app enters split view or a foldable opens.

How it works

Size class and hinge come from a Nitro module, OneAdaptive. When one is imported it reads both synchronously from native code, so the first render never shows a default and then flips. The first component that subscribes starts the native monitor; when the last one unmounts the monitor stops, and the next subscriber reads a fresh value.

Reserved regions come from a native view, One.UI.ReservedRegions.Provider, that reports the regions inside its own bounds. On iOS 27.1 they are UIKit’s reservedRegions and update as the hinge moves. On Android a fold from Jetpack WindowManager is a division, active while it separates or fully covers the display and inactive while the device lies flat, and each display cutout is an occlusion. Window segments are computed in JavaScript: each active division that spans the provider edge to edge cuts it in two.

API reference

Hooks and readersTypeDescription
useSizeClass() => SizeClassThe window size class, live.
() => SizeClassThe window size class, live.
getSizeClass() => Promise<SizeClass>Read the size class once.
() => Promise<SizeClass>Read the size class once.
useHinge() => HingeState | nullThe hinge, live. null on a device without one.
() => HingeState | nullThe hinge, live. null on a device without one.
getHinge() => Promise<HingeState | null>Read the hinge once.
() => Promise<HingeState | null>Read the hinge once.
onHingeChange(callback: (hinge: HingeState | null) => void) => () => voidSubscribe outside React. Returns the unsubscribe function.
(callback: (hinge: HingeState | null) => void) => () => voidSubscribe outside React. Returns the unsubscribe function.
useReservedRegions(options?: ReservedRegionOptions) => readonly ReservedRegion[]Regions in the nearest provider. Active ones only unless includeInactive is set.
(options?: ReservedRegionOptions) => readonly ReservedRegion[]Regions in the nearest provider. Active ones only unless includeInactive is set.
useReservedRegionsReady() => booleanTrue once the provider has its first native reading, even an empty one.
() => booleanTrue once the provider has its first native reading, even an empty one.
useWindowSegments() => readonly WindowSegment[]The free parts of the provider. Empty until ready.
() => readonly WindowSegment[]The free parts of the provider. Empty until ready.
useSpanning() => booleanWhether an active division splits the provider.
() => booleanWhether an active division splits the provider.
SizeClassTypeDescription
horizontalUserInterfaceSizeClasscompact, regular, or unspecified.
UserInterfaceSizeClasscompact, regular, or unspecified.
verticalUserInterfaceSizeClasscompact, regular, or unspecified.
UserInterfaceSizeClasscompact, regular, or unspecified.
HingeStateTypeDescription
statusHingeStatusunknown, closed, partiallyOpen, or fullyOpen.
HingeStatusunknown, closed, partiallyOpen, or fullyOpen.
anglenumberThe hinge angle in radians.
numberThe hinge angle in radians.
ReservedRegionTypeDescription
idstringStable for the life of the region.
stringStable for the life of the region.
kindReservedRegionKinddivision: a fold not to straddle. occlusion: a cutout or control that hides content.
ReservedRegionKinddivision: a fold not to straddle. occlusion: a cutout or control that hides content.
frame{ x: number; y: number; width: number; height: number }The region including margins, in the provider's points.
{ x: number; y: number; width: number; height: number }The region including margins, in the provider's points.
margins{ top: number; left: number; bottom: number; right: number }Room kept around the region for interactive content.
{ top: number; left: number; bottom: number; right: number }Room kept around the region for interactive content.
isActivebooleanWhether the region applies right now.
booleanWhether the region applies right now.
ReservedRegionOptionsTypeDescription
kindReservedRegionKindReturn only divisions or only occlusions.
ReservedRegionKindReturn only divisions or only occlusions.
includeInactivebooleanAlso return regions that do not apply right now.
booleanAlso return regions that do not apply right now.

Platform differences

iOSAndroidWeb
Size classScene traitsWidth and height breakpointsAlways regular
HingeiOS 27.1 and laterFold posture and hinge sensorAlways null
Reserved regionsiOS 27.1 and laterFolds and cutoutsNone
Region marginsReportedAlways 0None

Limits and gotchas

Examples

Lay out around a fold

import { View } from 'react-native'
import { One, useWindowSegments, type WindowSegment } from 'one'
function Board() {
const segments = useWindowSegments()
if (segments.length < 2) return <Canvas />
const [first, second] = segments
return (
<View style={{ flex: 1 }}>
<View style={place(first)}><Canvas /></View>
<View style={place(second)}><Palette /></View>
</View>
)
}
function place({ x, y, width, height }: WindowSegment) {
return { position: 'absolute', left: x, top: y, width, height } as const
}
export default function Screen() {
return (
<One.UI.ReservedRegions.Provider style={{ flex: 1 }}>
<Board />
</One.UI.ReservedRegions.Provider>
)
}

Each segment is a rect in the provider’s coordinates, so it maps straight to an absolutely positioned view. When the device is not split, there is one segment and the canvas fills the screen.

React to the hinge outside React

import { onHingeChange } from 'one'
const stop = onHingeChange((hinge) => {
if (hinge?.status === 'partiallyOpen') pauseVideo()
})

The callback gets null on a device without a hinge, so check before reading status.

ArrangementView

One.iOS.ArrangementView hosts SwiftUI’s ArrangementView on iOS 27.1 and later, and throws on earlier versions. It places a primary and a secondary pane side by side (split), one over the other (overlay), or lets SwiftUI choose for the window (automatic). On web it renders the two panes in a row.

import { One } from 'one'
function Workspace() {
return (
<One.iOS.ArrangementView
arrangementViewStyle="split"
splitArrangementLayoutRatio={0.5}
primary={<Chat />}
secondary={<Preview />}
/>
)
}

Each pane can also be a child, ArrangementView.Primary and ArrangementView.Secondary, which take the same split and overlay props to size that pane alone. Leading and Detail are aliases for them.

One.iOS.ArrangementViewTypeDescription
primaryReactNodeThe first pane.
ReactNodeThe first pane.
secondaryReactNodeThe second pane.
ReactNodeThe second pane.
leadingReactNodeAlias of primary.
ReactNodeAlias of primary.
detailReactNodeAlias of secondary.
ReactNodeAlias of secondary.
childrenReactNodePrimary and Secondary panes, as an alternative to the props.
ReactNodePrimary and Secondary panes, as an alternative to the props.
arrangementViewStyleArrangementViewStyle = "automatic"automatic, split, or overlay, optionally with the axes it applies to.
ArrangementViewStyle = "automatic"automatic, split, or overlay, optionally with the axes it applies to.
splitArrangementLayoutRatioSplitLayoutRatioThe share of space for the split panes, as a number or per-axis bounds.
SplitLayoutRatioThe share of space for the split panes, as a number or per-axis bounds.
splitArrangementLayoutSizeSplitLayoutSizeMinimum, ideal, and maximum pane sizes in points.
SplitLayoutSizeMinimum, ideal, and maximum pane sizes in points.
splitArrangementFixedLayoutSizeSplitFixedLayoutSizeKeep the split size fixed, per axis.
SplitFixedLayoutSizeKeep the split size fixed, per axis.
overlayArrangementEdgeOverlayArrangementEdgeWhere the overlay pane sits: top or bottom.
OverlayArrangementEdgeWhere the overlay pane sits: top or bottom.
onHingeChange(hinge: HingeState | null) => voidThe hinge changed while this view is mounted.
(hinge: HingeState | null) => voidThe hinge changed while this view is mounted.
styleStyleProp<ViewStyle>Defaults to flex: 1.
StyleProp<ViewStyle>Defaults to flex: 1.
swiftStyleOneNativeStyleSwiftUI modifiers for the arrangement.
OneNativeStyleSwiftUI modifiers for the arrangement.
testIDstringTest identifier.
stringTest identifier.

Edit this page on GitHub.