androidx.xr.compose.spatial

Interfaces

ContentEdge

An enum that represents the edges of a view where an orbiter can be placed.

Classes

ContentEdge.Horizontal
ContentEdge.Vertical

Represents vertical edges (start or end).

OrbiterOffsetType

Represents the type of offset used for positioning an orbiter.

SpatialDialogProperties

Properties for configuring a SpatialDialog.

Objects

OrbiterDefaults

Contains default values used by Orbiters.

SpatialElevationLevel

Defines standardized resting elevation levels for spatial UI elements.

Annotations

ExperimentalUserSubspaceApi

Marks Subspace APIs that are experimental and likely to change or be removed in the future.

Top-level functions summary

Unit
@Composable
@ComposableOpenTarget(index = -1)
AnchoredSubspace(
    lockTo: AnchorEntity,
    modifier: SubspaceModifier,
    allowUnboundedSubspace: Boolean,
    content: @Composable @SubspaceComposable SpatialBoxScope.() -> Unit
)

Creates an ApplicationSubspace that places its content at a real-world location represented by AnchorEntity.

Unit
@Composable
@ComposableOpenTarget(index = -1)
ApplicationSubspace(
    modifier: SubspaceModifier,
    allowUnboundedSubspace: Boolean,
    content: @Composable @SubspaceComposable SpatialBoxScope.() -> Unit
)

This function is deprecated. Use the Subspace API instead.

Unit
@Composable
@ComposableOpenTarget(index = -1)
Orbiter(
    position: ContentEdge.Horizontal,
    offset: Dp,
    offsetType: OrbiterOffsetType,
    alignment: Alignment.Horizontal,
    shape: SpatialShape,
    elevation: Dp,
    shouldRenderInNonSpatial: Boolean,
    content: @Composable @UiComposable () -> Unit
)

A composable that creates an orbiter along the top or bottom edges of a view.

Unit
@Composable
@ComposableOpenTarget(index = -1)
Orbiter(
    position: ContentEdge.Vertical,
    offset: Dp,
    offsetType: OrbiterOffsetType,
    alignment: Alignment.Vertical,
    shape: SpatialShape,
    elevation: Dp,
    shouldRenderInNonSpatial: Boolean,
    content: @Composable @UiComposable () -> Unit
)

A composable that creates an orbiter along the start or end edges of a view.

Unit

Creates a 3D space for spatial content that is embedded within and positioned by a 2D container.

Unit
@Composable
SpatialDialog(
    onDismissRequest: () -> Unit,
    properties: SpatialDialogProperties,
    content: @Composable () -> Unit
)

SpatialDialog is a dialog that is elevated above the activity.

Unit
@Composable
SpatialElevation(elevation: Dp, content: @Composable () -> Unit)

Composable that creates a panel in 3D space when spatialization is enabled.

Unit
@Composable
SpatialPopup(
    alignment: Alignment,
    offset: IntOffset,
    onDismissRequest: (() -> Unit)?,
    elevation: Dp,
    properties: PopupProperties,
    content: @Composable () -> Unit
)

A composable that creates a panel in 3D space to hoist Popup based composables.

Unit
@Composable
@ComposableOpenTarget(index = -1)
Subspace(
    modifier: SubspaceModifier,
    allowUnboundedSubspace: Boolean,
    content: @Composable @SubspaceComposable SpatialBoxScope.() -> Unit
)

Create a 3D area that the app can render spatial content into.

Unit
@Composable
@ComposableOpenTarget(index = -1)
@ExperimentalUserSubspaceApi
UserSubspace(
    modifier: SubspaceModifier,
    lockTo: BodyPart,
    lockDimensions: LockDimensions,
    behavior: LockingBehavior,
    allowUnboundedSubspace: Boolean,
    content: @Composable @SubspaceComposable SpatialBoxScope.() -> Unit
)

Create a user-centric 3D space that is ideal for spatial UI content that follows the user's given body part with configurable following behaviors.

Top-level functions

AnchoredSubspace

@Composable
@ComposableOpenTarget(index = -1)
fun AnchoredSubspace(
    lockTo: AnchorEntity,
    modifier: SubspaceModifier = SubspaceModifier,
    allowUnboundedSubspace: Boolean = false,
    content: @Composable @SubspaceComposable SpatialBoxScope.() -> Unit
): Unit

Creates an ApplicationSubspace that places its content at a real-world location represented by AnchorEntity.

This is useful for placing UI elements on real-world surfaces or at specific spatial locations. The visual stability of the anchored content depends on the underlying system's ability to track the AnchorEntity.

AnchoredSubspace follows the same conventions as ApplicationSubspace, including layout and sizing behaviors. See ApplicationSubspace for more details.

Note: For Creating, loading, and persisting anchors, please check androidx.xr.scenecore.AnchorEntity for more information

import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.xr.arcore.Anchor
import androidx.xr.arcore.AnchorCreateSuccess
import androidx.xr.compose.platform.LocalSession
import androidx.xr.compose.spatial.AnchoredSubspace
import androidx.xr.compose.subspace.SpatialMainPanel
import androidx.xr.compose.subspace.SpatialPanel
import androidx.xr.compose.subspace.SpatialRow
import androidx.xr.compose.subspace.layout.SubspaceModifier
import androidx.xr.compose.subspace.layout.rotate
import androidx.xr.runtime.Session
import androidx.xr.runtime.math.Pose
import androidx.xr.scenecore.AnchorEntity

val TAG = "AnchoredSubspaceSample"
@Composable
fun MainPanelContent() {
    Text("Main panel")
}

@Composable
fun AppContent() {
    MainPanelContent()

    var session: Session? = LocalSession.current
    if (session == null) return

    val anchor =
        remember(session) {
            when (val anchorResult = Anchor.create(session, Pose.Identity)) {
                is AnchorCreateSuccess -> AnchorEntity.create(session, anchorResult.anchor)
                else -> {
                    Log.e(TAG, "Failed to create anchor: ${anchorResult::class.simpleName}")
                    null
                }
            }
        }
    if (anchor != null) {
        AnchoredSubspace(
            lockTo = anchor,
            modifier = SubspaceModifier.rotate(pitch = -90f, 0f, 0f),
        ) {
            SpatialRow {
                SpatialPanel { Text("Spatial panel") }
                SpatialMainPanel()
            }
        }
        DisposableEffect(anchor) { onDispose { anchor.dispose() } }
    }
}
Parameters
lockTo: AnchorEntity

the real-world AnchorEntity to which this space will be attached. If the developer changes the anchor parameter then the subspace will be reanchored to the swapped anchor.

modifier: SubspaceModifier = SubspaceModifier

The SubspaceModifier to be applied to this Subspace.

allowUnboundedSubspace: Boolean = false

If true, the default recommended content box constraints will not be applied, allowing the Subspace to be infinite. Defaults to false, providing a safe, bounded space.

content: @Composable @SubspaceComposable SpatialBoxScope.() -> Unit

The content to render within this Subspace.

ApplicationSubspace

@Composable
@ComposableOpenTarget(index = -1)
fun ApplicationSubspace(
    modifier: SubspaceModifier = SubspaceModifier,
    allowUnboundedSubspace: Boolean = false,
    content: @Composable @SubspaceComposable SpatialBoxScope.() -> Unit
): Unit

Create a 3D area that the app can render spatial content into.

ApplicationSubspace creates a Compose for XR's Spatial UI hierarchy (3D Scene Graph) in your application's regular Compose UI tree. In this Subspace, You can use a @SubspaceComposable to create 3D UI elements.

Each call to ApplicationSubspace creates a new, independent Spatial UI hierarchy. It does not inherit the spatial position, orientation, or scale of any parent ApplicationSubspace it is nested within. Its position and scale are solely decided by the system's recommended position and scale. To create an embedded Subspace within a SpatialPanel, Orbiter, SpatialPopup and etc, use the Subspace instead.

By default, this Subspace is automatically bounded by the system's recommended content box. This box represents a comfortable, human-scale area in front of the user, sized to occupy a significant portion of their view on any given device. Using this default is the suggested way to create responsive spatial layouts that look great without hardcoding dimensions. SubspaceModifiers like SubspaceModifier.fillMaxSize will expand to fill this recommended box. This default can be overridden by applying a custom size-based modifier. For unbounded behavior, set allowUnboundedSubspace = true.

This composable is a no-op and does not render anything in non-XR environments (i.e., Phone and Tablet).

On XR devices that cannot currently render spatial UI, the ApplicationSubspace will still create its scene and all of its internal state, even though nothing may be rendered. This is to ensure that the state is maintained consistently in the spatial scene and to allow preparation for the support of rendering spatial UI. State should be maintained by the compose runtime and events that cause the compose runtime to lose state (app process killed or configuration change) will also cause the ApplicationSubspace to lose its state.

Parameters
modifier: SubspaceModifier = SubspaceModifier

The SubspaceModifier to be applied to the content of this Subspace.

allowUnboundedSubspace: Boolean = false

If true, the default recommended content box constraints will not be applied, allowing the Subspace to be infinite. Defaults to false, providing a safe, bounded space.

content: @Composable @SubspaceComposable SpatialBoxScope.() -> Unit

The 3D content to render within this Subspace.

@Composable
@ComposableOpenTarget(index = -1)
fun Orbiter(
    position: ContentEdge.Horizontal,
    offset: Dp = 0.dp,
    offsetType: OrbiterOffsetType = OrbiterOffsetType.OuterEdge,
    alignment: Alignment.Horizontal = Alignment.CenterHorizontally,
    shape: SpatialShape = OrbiterDefaults.Shape,
    elevation: Dp = OrbiterDefaults.Elevation,
    shouldRenderInNonSpatial: Boolean = true,
    content: @Composable @UiComposable () -> Unit
): Unit

A composable that creates an orbiter along the top or bottom edges of a view.

Orbiters are floating elements that are typically used to control the content within spatial panels and other entities that they're anchored to. They allow the content to have more space and give users quick access to features like navigation without obstructing the main content.

The size of the Orbiter is constrained by the dimensions of the parent spatial component it is anchored to (e.g., a androidx.xr.compose.subspace.SpatialPanel). If it's not placed within a specific spatial component, it defaults to the main window's size. Consequently, an Orbiter's content cannot be larger than its parent's dimensions.

Parameters
position: ContentEdge.Horizontal

The edge of the orbiter. Use ContentEdge.Top or ContentEdge.Bottom.

offset: Dp = 0.dp

The offset of the orbiter based on the outer edge of the orbiter.

offsetType: OrbiterOffsetType = OrbiterOffsetType.OuterEdge

The type of offset used for positioning the orbiter.

alignment: Alignment.Horizontal = Alignment.CenterHorizontally

The alignment of the orbiter. Use Alignment.CenterHorizontally or Alignment.Start or Alignment.End.

shape: SpatialShape = OrbiterDefaults.Shape

The shape of this Orbiter when it is rendered in 3D space.

elevation: Dp = OrbiterDefaults.Elevation

The z-direction elevation level of this Orbiter.

shouldRenderInNonSpatial: Boolean = true

In a non-spatial environment, if true the orbiter content is rendered as if the orbiter wrapper was not present and removed from the flow otherwise. In spatial environments, this flag is ignored.

content: @Composable @UiComposable () -> Unit

The content of the orbiter.

Example:

Orbiter(position = ContentEdge.Top, offset = 10.dp) {
Text("This is a top edge Orbiter")
}
@Composable
@ComposableOpenTarget(index = -1)
fun Orbiter(
    position: ContentEdge.Vertical,
    offset: Dp = 0.dp,
    offsetType: OrbiterOffsetType = OrbiterOffsetType.OuterEdge,
    alignment: Alignment.Vertical = Alignment.CenterVertically,
    shape: SpatialShape = OrbiterDefaults.Shape,
    elevation: Dp = OrbiterDefaults.Elevation,
    shouldRenderInNonSpatial: Boolean = true,
    content: @Composable @UiComposable () -> Unit
): Unit

A composable that creates an orbiter along the start or end edges of a view.

Orbiters are floating elements that are typically used to control the content within spatial panels and other entities that they're anchored to. They allow the content to have more space and give users quick access to features like navigation without obstructing the main content.

The size of the Orbiter is constrained by the dimensions of the parent spatial component it is anchored to (e.g., a androidx.xr.compose.subspace.SpatialPanel). If it's not placed within a specific spatial component, it defaults to the main window's size. Consequently, an Orbiter's content cannot be larger than its parent's dimensions.

Parameters
position: ContentEdge.Vertical

The edge of the orbiter. Use ContentEdge.Start or ContentEdge.End.

offset: Dp = 0.dp

The offset of the orbiter based on the outer edge of the orbiter.

offsetType: OrbiterOffsetType = OrbiterOffsetType.OuterEdge

The type of offset used for positioning the orbiter.

alignment: Alignment.Vertical = Alignment.CenterVertically

The alignment of the orbiter. Use Alignment.CenterVertically or Alignment.Top or Alignment.Bottom.

shape: SpatialShape = OrbiterDefaults.Shape

The shape of this Orbiter when it is rendered in 3D space.

elevation: Dp = OrbiterDefaults.Elevation

The z-direction elevation level of this Orbiter.

shouldRenderInNonSpatial: Boolean = true

In a non-spatial environment, if true the orbiter content is rendered as if the orbiter wrapper was not present and removed from the flow otherwise. In spatial environments, this flag is ignored.

content: @Composable @UiComposable () -> Unit

The content of the orbiter.

Example:

Orbiter(position = ContentEdge.Start, offset = 10.dp) {
Text("This is a start edge Orbiter")
}

PlanarEmbeddedSubspace

@Composable
@UiComposable
fun PlanarEmbeddedSubspace(content: @Composable @SubspaceComposable SpatialBoxScope.() -> Unit): Unit

Creates a 3D space for spatial content that is embedded within and positioned by a 2D container.

A PlanarEmbeddedSubspace acts as a bridge between a 2D layout context and a 3D spatial scene. It must be placed within a composable that provides a 2D surface in the 3D world, such as SpatialPanel, Orbiter, or a custom component built on similar principles.

The PlanarEmbeddedSubspace itself is laid out like a regular 2D composable, respecting the constraints and positioning of its parent. The 3D content placed inside it is then positioned relative to this 2D-defined area.

Key behaviors:

  • Layout: The width and height are determined by the parent 2D layout. The depth (Z-axis) constraints are inherited from the surrounding spatial environment, allowing content to extend forwards and backwards from the 2D surface.

  • Content: The content lambda is a @SubspaceComposable scope, where you can place 3D elements like SpatialBox.

  • Environment: This composable is a no-op and renders nothing in non-XR environments (e.g., phones and tablets).

import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import androidx.xr.compose.spatial.PlanarEmbeddedSubspace
import androidx.xr.compose.subspace.SpatialPanel
import androidx.xr.compose.subspace.SubspaceComposable
import androidx.xr.compose.subspace.layout.SubspaceModifier
import androidx.xr.compose.subspace.layout.offset

@Composable
@SubspaceComposable
fun PanelWithPlanarEmbeddedSubspace() {
    // A PlanarEmbeddedSubspace is placed inside a SpatialPanel.
    // The 3D content inside the PlanarEmbeddedSubspace is positioned
    // relative to the 2D area of the SpatialPanel.
    // Here we place it in a Row to demonstrate that it participates in 2D layout.
    SpatialPanel {
        Row {
            Text("2D content")

            PlanarEmbeddedSubspace {
                // The content of a PlanarEmbeddedSubspace must be a SubspaceComposable.
                // Here we use another SpatialPanel to host 2D content. However,
                // this could be any 3D content (i.e. glTFs).
                SpatialPanel(SubspaceModifier.offset(z = (-50).dp)) {
                    Text("Embedded 3D content")
                }
            }
        }
    }
}
Parameters
content: @Composable @SubspaceComposable SpatialBoxScope.() -> Unit

The @SubspaceComposable 3D content to render within this subspace.

See also
Subspace

For creating a top-level, application-anchored spatial scene.

SpatialDialog

@Composable
fun SpatialDialog(
    onDismissRequest: () -> Unit,
    properties: SpatialDialogProperties = SpatialDialogProperties(),
    content: @Composable () -> Unit
): Unit

SpatialDialog is a dialog that is elevated above the activity.

When spatial dialogs are displayed the dialog appears on top of the content at the base elevation level.

In non-spatialized environments, a standard Compose Dialog is utilized to display the content.

Parameters
onDismissRequest: () -> Unit

a callback to be invoked when the dialog should be dismissed.

properties: SpatialDialogProperties = SpatialDialogProperties()

the dialog properties.

content: @Composable () -> Unit

the content of the dialog.

SpatialElevation

@Composable
fun SpatialElevation(
    elevation: Dp = SpatialElevationLevel.Level0,
    content: @Composable () -> Unit
): Unit

Composable that creates a panel in 3D space when spatialization is enabled.

SpatialElevation elevates content in-place. It uses the source position and constraints to determine the size and placement of the elevated panel while reserving space for the original element within the layout.

In non-spatial environments, the content is rendered normally without elevation.

SpatialElevation does not support a content lambda that has a width or height of zero.

Parameters
elevation: Dp = SpatialElevationLevel.Level0

the desired elevation level for the panel in spatial environments.

content: @Composable () -> Unit

the composable content to be displayed within the elevated panel.

SpatialPopup

@Composable
fun SpatialPopup(
    alignment: Alignment = Alignment.TopStart,
    offset: IntOffset = IntOffset(0, 0),
    onDismissRequest: (() -> Unit)? = null,
    elevation: Dp = SpatialElevationLevel.Level3,
    properties: PopupProperties = PopupProperties(),
    content: @Composable () -> Unit
): Unit

A composable that creates a panel in 3D space to hoist Popup based composables.

Parameters
alignment: Alignment = Alignment.TopStart

the alignment of the popup relative to its parent.

offset: IntOffset = IntOffset(0, 0)

An offset from the original aligned position of the popup. Offset respects the Ltr/Rtl context, thus in Ltr it will be added to the original aligned position and in Rtl it will be subtracted from it.

onDismissRequest: (() -> Unit)? = null

callback invoked when the user requests to dismiss the popup (e.g., by clicking outside).

elevation: Dp = SpatialElevationLevel.Level3

the elevation value of the SpatialPopUp.

properties: PopupProperties = PopupProperties()

PopupProperties configuration properties for further customization of this popup's behavior.

content: @Composable () -> Unit

the composable content to be displayed within the popup.

@Composable
@ComposableOpenTarget(index = -1)
fun Subspace(
    modifier: SubspaceModifier = SubspaceModifier,
    allowUnboundedSubspace: Boolean = false,
    content: @Composable @SubspaceComposable SpatialBoxScope.() -> Unit
): Unit

Create a 3D area that the app can render spatial content into.

Subspace creates a Compose for XR Spatial UI hierarchy (3D Scene Graph) in your application's regular Compose UI tree. In this Subspace, You can use a @SubspaceComposable annotated composable functions to create 3D UI elements.

Each call to Subspace creates a new, independent Spatial UI hierarchy. It does not inherit the spatial position, orientation, or scale of any parent Subspace it is nested within. Its position and scale are solely decided by the system's recommended position and scale. To create an embedded Subspace within a SpatialPanel, Orbiter, SpatialPopup and etc, use the PlanarEmbeddedSubspace instead.

By default, this Subspace is automatically bounded by the system's recommended content box. This box represents a comfortable, human-scale area in front of the user, sized to occupy a significant portion of their view on any given device. Using this default is the suggested way to create responsive spatial layouts that look great without hardcoding dimensions. SubspaceModifiers like SubspaceModifier.fillMaxSize will expand to fill this recommended box. This default can be overridden by applying a custom size-based modifier. For unbounded behavior, set allowUnboundedSubspace = true.

This composable is a no-op and does not render anything in non-XR environments (i.e., Phone and Tablet).

On XR devices that cannot currently render spatial UI, the Subspace will still create its scene and all of its internal state, even though nothing may be rendered. This is to ensure that the state is maintained consistently in the spatial scene and to allow preparation for the support of rendering spatial UI. State should be maintained by the compose runtime and events that cause the compose runtime to lose state (app process killed or configuration change) will also cause the Subspace to lose its state.

Parameters
modifier: SubspaceModifier = SubspaceModifier

The SubspaceModifier to be applied to the content of this Subspace.

allowUnboundedSubspace: Boolean = false

If true, the default recommended content box constraints will not be applied, allowing the Subspace to be infinite. Defaults to false, providing a safe, bounded space.

content: @Composable @SubspaceComposable SpatialBoxScope.() -> Unit

The 3D content to render within this Subspace.

UserSubspace

@Composable
@ComposableOpenTarget(index = -1)
@ExperimentalUserSubspaceApi
fun UserSubspace(
    modifier: SubspaceModifier = SubspaceModifier,
    lockTo: BodyPart = BodyPart.Head,
    lockDimensions: LockDimensions = LockDimensions.All,
    behavior: LockingBehavior = LockingBehavior.lazy(),
    allowUnboundedSubspace: Boolean = false,
    content: @Composable @SubspaceComposable SpatialBoxScope.() -> Unit
): Unit

Create a user-centric 3D space that is ideal for spatial UI content that follows the user's given body part with configurable following behaviors.

Each call to UserSubspace creates a new, independent spatial UI hierarchy. It does not inherit the spatial position, orientation, or scale of any parent Subspace it is nested within. Its position in the world is determined solely by its lockTo parameter.

By default, this Subspace is automatically bounded by the system's recommended content box, similar to Subspace. When using BodyPart.Head as the lockTo target, this API requires headtracking to not be disabled in the session configuration. If it is disabled, this API will not return anything. The session configuration should resemble session.configure( config = session.config.copy(headTracking = Config.HeadTrackingMode.LAST_KNOWN) )

This composable is a no-op in non-XR environments (i.e., Phone and Tablet).

Managing Spatial Overlap

Because each call to any kind of Subspace function creates an independent 3D scene, these spaces are not aware of one another. This can lead to a phenomenon known as the "tunneling effect," where a moving UserSubspace (like a head-locked menu) can intersect with content in another stationary Subspace. This overlap can cause jarring visual artifacts and z-depth ordering issues (Z-fighting), creating a confusing user experience. A Subspace does not perform automatic collision avoidance between these independent Subspaces. It is the developer's responsibility to manage the layout and prevent these intersections or to introduce custom hit handling.

Guidelines for Preventing Overlap:

  1. Control Volume Size: Carefully define the bounds of your Subspace instances. Instead of letting content fill the maximum recommended constraints, use sizing modifiers to create smaller, manageable content areas that are less likely to collide.

  2. Use Strategic Offsets: Use SubspaceModifier.offset to position a Subspace. For example, a head-locked menu can be offset to appear in the user's peripheral vision, reducing the chance it will collide with central content.Also, consider placing different Subspace instances at different depths. This ensures that if they overlap, their z-depth ordering will be clear and predictable. Note, however, that while the visual ordering may be clear, Jetpack XR doesn't guarantee predictable interaction behaviors between UI elements in separate, overlapping Subspaces.

Parameters
modifier: SubspaceModifier = SubspaceModifier

The SubspaceModifier to be applied to the content of this Subspace.

lockTo: BodyPart = BodyPart.Head

Specifies a part of the body which the Subspace will be locked to.

lockDimensions: LockDimensions = LockDimensions.All

A set of boolean flags to determine the dimensions of movement that are tracked. Possible tracking dimensions are: translationX, translationY, translationZ, rotationX, rotationY, and rotationZ. By default, all dimensions are tracked. Any dimensions not listed will not be tracked. For example if translationY is not listed, this means the content will not move as the user moves vertically up and down.

behavior: LockingBehavior = LockingBehavior.lazy()

determines how the UserSubspace follows the user. It can be made to move faster and be more responsive. The default is LockingBehavior.lazy().

allowUnboundedSubspace: Boolean = false

If true, the default recommended content box constraints will not be applied, allowing the Subspace to be infinite. Defaults to false, providing a safe, bounded space.

content: @Composable @SubspaceComposable SpatialBoxScope.() -> Unit

The 3D content to render within this Subspace.