Fallback and compatibility
Updated: Sep 2, 2026
After you
set up the Meta VR Layout SDK, choose how each window behaves when spatial placement is not available. A window can fall back because of the current host, the Horizon OS release, or a lack of platform capacity. This is the compatibility path on phones and other Android devices without spatial window support. The main window remains available while each spatial window applies its configured fallback.
Choose a fallback strategy
WindowFallback.Inline is the default. It renders content at the SpatialWindow declaration site when the window cannot receive a spatial slot, so content always has a place to render unless you choose WindowFallback.Drop. In React Native, the equivalent values are inline and drop.
The following examples place a details panel after the main content in a row. When spatial placement is unavailable, inline fallback keeps the details panel in that position.
import androidx.compose.foundation.layout.Row
import androidx.compose.ui.unit.dp
import metavrx.layout.window.compose.SpatialWindow
import metavrx.layout.window.compose.layout.WindowAnchor
import metavrx.layout.window.compose.layout.WindowModifier
import metavrx.layout.window.compose.layout.anchor
import metavrx.layout.window.compose.layout.size
import metavrx.layout.window.compose.state.WindowFallback
Row {
MainContent()
SpatialWindow(
key = "details",
modifier = WindowModifier
.size(320.dp, 480.dp)
.anchor(WindowAnchor.End),
) { DetailsPanel() }
}
import {SpatialWindow} from '@metavr/layout-window-compat';
import {View} from 'react-native';
<View style={ {flexDirection: 'row'} }>
<MainContent />
<SpatialWindow
label="details"
windowWidth={320}
windowHeight={480}
anchor={ {parent: 'end', child: 'start'} }>
<DetailsPanel />
</SpatialWindow>
</View>
Design the declaration-site layout intentionally when using inline fallback. The same content can appear in a spatial window on a supported host and in the normal app layout when spatial placement is unavailable.
Use LocalFallbackStrategy to change the default for every SpatialWindow within part of your Compose UI tree. A fallbackStrategy value on an individual SpatialWindow overrides the composition local.
import androidx.compose.runtime.CompositionLocalProvider
import metavrx.layout.window.compose.LocalFallbackStrategy
// Overrides the default for every SpatialWindow in this composition subtree.
CompositionLocalProvider(
LocalFallbackStrategy provides WindowFallback.Drop,
) {
// Inherits WindowFallback.Drop from LocalFallbackStrategy.
SpatialWindow(
key = "optional-panel",
modifier = WindowModifier
.size(320.dp, 480.dp)
.anchor(WindowAnchor.Start),
) {
OptionalPanel()
}
SpatialWindow(
key = "details",
modifier = WindowModifier
.size(320.dp, 480.dp)
.anchor(WindowAnchor.End),
// Overrides the composition default for this window.
fallbackStrategy = WindowFallback.Inline,
) {
DetailsPanel()
}
}
Each React Native SpatialWindow uses inline fallback unless you set its fallback prop to drop. Set the prop on each window that needs different behavior.
<View>
<SpatialWindow
label="optional-panel"
fallback="drop"
windowWidth={320}
windowHeight={480}
anchor=>
<OptionalPanel />
</SpatialWindow>
<SpatialWindow
label="details"
fallback="inline"
windowWidth={320}
windowHeight={480}
anchor=>
<DetailsPanel />
</SpatialWindow>
</View>
Observe placement outside window content
A fallback strategy controls what a SpatialWindow renders when it does not hold a spatial slot. Observe window state when the rest of your UI also needs to respond, such as by reserving space in the main panel, showing a custom fallback, or recording placement outcomes.
LocalPromoted.current reports whether the current SpatialWindow content is placed or inline. Because it is read from inside the window content, it cannot drive UI elsewhere. With WindowFallback.Drop, there is no unplaced content to read it. Remember a WindowState outside the content, pass it to SpatialWindow, and collect its lifecycle instead.
@Composable
fun DetailsLayout() {
val detailsState = rememberWindowState()
val lifecycle by detailsState.lifecycle.collectAsState()
val isDetailsPlaced by remember { derivedStateOf { lifecycle == WindowLifecycle.Active } }
LaunchedEffect(detailsState) {
detailsState.lifecycle.collect { currentLifecycle ->
val failure = currentLifecycle as? WindowLifecycle.Failure.Rejected
if (failure != null) {
reportPlacementFailure(failure.code)
}
}
}
Row {
MainContent()
if (!isDetailsPlaced) {
DetailsFallback()
}
SpatialWindow(
key = "details",
modifier = WindowModifier
.size(320.dp, 480.dp)
.anchor(WindowAnchor.End),
windowState = detailsState,
fallbackStrategy = WindowFallback.Drop,
) {
DetailsPanel()
}
}
}
WindowLifecycle.Active means the window is placed. Pending and Preempted are waiting states that the SDK resolves when the window wins a slot. Failure.Transient is retried automatically, while Failure.Rejected is terminal. ExternallyRemoved means the user or system closed the window. derivedStateOf updates the layout only when the placed value changes. Collect the lifecycle separately for side effects such as reporting a rejection.
Call useSpatialWindowState outside the window content and pass the same stable label to the hook and SpatialWindow. Because the hook observes scene placement state, it continues to report the window when fallback="drop" prevents the window content from rendering.
function DetailsLayout() {
const {placement} = useSpatialWindowState('details');
const isDetailsPlaced = placement === 'spatial';
return (
<View style=>
<MainContent />
{!isDetailsPlaced && <DetailsFallback />}
<SpatialWindow
label="details"
fallback="drop"
windowWidth={320}
windowHeight={480}
anchor=>
<DetailsPanel />
</SpatialWindow>
</View>
);
}
The hook reports spatial when the window is placed, pending while placement is in progress, inline for an inline fallback or before the first placement report, and dropped when the drop fallback is active. Its wasEverInline value records whether the window has rendered inline before; it does not report current placement. React Native exposes placement state, but not the underlying platform failure code.
Check spatial availability
Window state reports what happened to one window. To check whether the current host supports spatial placement at all, read LocalSpatialSupported.current at scene level in Compose. A non-activity SpatialScene host or a SpatialWindow outside a scene throws an exception.
In React Native, read useSpatialScene().isSpatialAvailable. Initialization failures report spatial placement as unavailable, but incompatible JavaScript and native major or minor versions cause the provider to throw.