Screen orientation
@symbiote-native/screen-orientation wraps
expo-screen-orientation —
orientation locking, reading the current orientation and lock, and an auto-updating
orientation-change subscription — so every SymbioteNative adapter can reach it, not just React.
Like network, it mixes stateless one-shot functions
(lockAsync/unlockAsync/getOrientationAsync/…) with exactly one live subscription:
useScreenOrientation seeds from one-shot getOrientationAsync()/getOrientationLockAsync()
calls and then subscribes to addOrientationChangeListener for live updates, the same
seed-then-subscribe shape as network’s own useNetworkState.
| OS platform | Support |
|---|---|
| iOS | ✅ live |
| Android | ✅ live |
| Framework adapter | Support |
|---|---|
| React | ✅ live |
| Vue | ✅ live |
| Angular | ✅ live |
| Svelte | ✅ live |
| Solid | ✅ live |
Installation
Section titled “Installation”npm install @symbiote-native/screen-orientationScaffolding or extending a SymbioteNative app? npx @symbiote-native/cli new --screen-orientation
(or add --screen-orientation in an existing app) installs and wires this for you — see
@symbiote-native/cli.
expo-screen-orientation and expo-modules-core come along as regular dependencies, pinned to
exact versions — never install either yourself, and never add the expo meta-package to your
project (it bundles its own Metro/Babel pipeline, which conflicts with this project’s own).
No platform permission string is needed — locking and reading the current orientation read/write system state with no runtime permission prompt on either platform.
Reactive orientation state
Section titled “Reactive orientation state”import { lockAsync, OrientationLock } from '@symbiote-native/screen-orientation';import { useScreenOrientation } from '@symbiote-native/screen-orientation/react';
export default function ScreenOrientationScreen() { const { orientation, orientationLock } = useScreenOrientation();
return ( <> <text>Orientation: {orientation}</text> <text>Lock: {orientationLock}</text> <button title="Lock landscape" onPress={() => lockAsync(OrientationLock.LANDSCAPE)} /> </> );}<script setup lang="ts">import { lockAsync, OrientationLock } from '@symbiote-native/screen-orientation';import { useScreenOrientation } from '@symbiote-native/screen-orientation/vue';
const screenOrientation = useScreenOrientation(); // Ref<ScreenOrientationState>
function onLockLandscape() { void lockAsync(OrientationLock.LANDSCAPE);}</script>
<template> <text>Orientation: {{ screenOrientation.orientation }}</text> <text>Lock: {{ screenOrientation.orientationLock }}</text> <button title="Lock landscape" @press="onLockLandscape" /></template>import { Component, inject } from '@angular/core';import { SYMBIOTE_ELEMENTS } from '@symbiote-native/angular';import { lockAsync, OrientationLock } from '@symbiote-native/screen-orientation';import { ScreenOrientationService } from '@symbiote-native/screen-orientation/angular';
@Component({ standalone: true, imports: [SYMBIOTE_ELEMENTS], template: ` <text>Orientation: {{ screenOrientation().orientation }}</text> <text>Lock: {{ screenOrientation().orientationLock }}</text> <button title="Lock landscape" (press)="onLockLandscape()" /> `,})export class ScreenOrientationScreen { readonly screenOrientation = inject(ScreenOrientationService).connect(); // Signal<ScreenOrientationState>
onLockLandscape(): void { void lockAsync(OrientationLock.LANDSCAPE); }}<script lang="ts"> import { lockAsync, OrientationLock } from '@symbiote-native/screen-orientation'; import { useScreenOrientation } from '@symbiote-native/screen-orientation/svelte';
const screenOrientation = useScreenOrientation(); // { readonly current: ScreenOrientationState }
function onLockLandscape(): void { void lockAsync(OrientationLock.LANDSCAPE); }</script>
<text>Orientation: {screenOrientation.current.orientation}</text><text>Lock: {screenOrientation.current.orientationLock}</text><button title="Lock landscape" onPress={onLockLandscape} />import { lockAsync, OrientationLock } from '@symbiote-native/screen-orientation';import { createScreenOrientation } from '@symbiote-native/screen-orientation/solid';
export default function ScreenOrientationScreen() { const screenOrientation = createScreenOrientation(); // Accessor<ScreenOrientationState>
function onLockLandscape(): void { void lockAsync(OrientationLock.LANDSCAPE); }
return ( <> <text>Orientation: {screenOrientation().orientation}</text> <text>Lock: {screenOrientation().orientationLock}</text> <button title="Lock landscape" onPress={onLockLandscape} /> </> );}Functions
Section titled “Functions”| Signature | Description |
|---|---|
lockAsync(orientationLock: OrientationLock): Promise<void> |
Locks the screen to the given OrientationLock. Throws a TypeError for an invalid value; a no-op for OrientationLock.OTHER |
lockPlatformAsync(options: PlatformOrientationInfo): Promise<void> |
Locks via a platform-specific param — an Android numeric constant, an iOS Orientation[], or a web WebOrientationLock string |
unlockAsync(): Promise<void> |
Unlocks the screen orientation back to OrientationLock.DEFAULT |
getOrientationAsync(): Promise<Orientation> |
Gets the device’s current screen orientation |
getOrientationLockAsync(): Promise<OrientationLock> |
Gets the current orientation lock, falling back to the last value set via lockAsync()/lockPlatformAsync() when the native method itself isn’t available |
getPlatformOrientationLockAsync(): Promise<PlatformOrientationInfo> |
Gets the current orientation lock as a platform-specific value |
supportsOrientationLockAsync(orientationLock: OrientationLock): Promise<boolean> |
Whether the given orientation lock is supported on this device |
addOrientationChangeListener(listener: OrientationChangeListener): EventSubscription |
Subscribes to orientation-change events — fires whenever orientation or lock changes. The primitive useScreenOrientation wraps |
removeOrientationChangeListener(subscription: EventSubscription): void |
Removes a single orientation-change listener’s subscription |
removeOrientationChangeListeners(): void |
Removes every orientation-change listener registered via addOrientationChangeListener() |
useScreenOrientation() config
Section titled “useScreenOrientation() config”Takes no arguments — the hook/composable/service seeds and subscribes automatically.
useScreenOrientation() return value
Section titled “useScreenOrientation() return value”| Field | Type | Description |
|---|---|---|
orientation |
Orientation |
The device’s current screen orientation, seeded Orientation.UNKNOWN until the first reading arrives |
orientationLock |
OrientationLock |
The current orientation lock, seeded OrientationLock.UNKNOWN until the first reading arrives |
React returns this shape as a plain value; Vue returns Ref<ScreenOrientationState>; Angular’s
ScreenOrientationService.connect() returns Signal<ScreenOrientationState>; Svelte returns
{ readonly current: ScreenOrientationState }, a boxed getter read as .current.
Orientation
Section titled “Orientation”| Member | Value | Description |
|---|---|---|
UNKNOWN |
0 |
The orientation could not be determined |
PORTRAIT_UP |
1 |
Right-side-up portrait |
PORTRAIT_DOWN |
2 |
Upside-down portrait |
LANDSCAPE_LEFT |
3 |
Landscape, rotated left |
LANDSCAPE_RIGHT |
4 |
Landscape, rotated right |
OrientationLock
Section titled “OrientationLock”| Member | Value | Description |
|---|---|---|
DEFAULT |
0 |
iOS: all orientations except PORTRAIT_DOWN. Android: system decides |
ALL |
1 |
All four possible orientations |
PORTRAIT |
2 |
Any portrait orientation |
PORTRAIT_UP |
3 |
Right-side-up portrait only |
PORTRAIT_DOWN |
4 |
Upside-down portrait only |
LANDSCAPE |
5 |
Any landscape orientation |
LANDSCAPE_LEFT |
6 |
Left landscape only |
LANDSCAPE_RIGHT |
7 |
Right landscape only |
OTHER |
8 |
A platform-specific orientation — not a valid lockAsync() policy |
UNKNOWN |
9 |
Unknown lock — not a valid lockAsync() policy |
ALL/PORTRAIT are invalid on devices that don’t support PORTRAIT_DOWN.
SizeClassIOS
Section titled “SizeClassIOS”| Member | Value | Description |
|---|---|---|
UNKNOWN |
0 |
The size class could not be determined |
COMPACT |
1 |
iOS compact UIKit size class |
REGULAR |
2 |
iOS regular UIKit size class |
WebOrientationLock / WebOrientation
Section titled “WebOrientationLock / WebOrientation”| Member | Value | Description |
|---|---|---|
PORTRAIT_PRIMARY |
'portrait-primary' |
The primary portrait orientation |
PORTRAIT_SECONDARY |
'portrait-secondary' |
The secondary (upside-down) portrait orientation |
PORTRAIT |
'portrait' |
Any portrait orientation. WebOrientationLock only |
LANDSCAPE_PRIMARY |
'landscape-primary' |
The primary landscape orientation |
LANDSCAPE_SECONDARY |
'landscape-secondary' |
The secondary (rotated) landscape orientation |
LANDSCAPE |
'landscape' |
Any landscape orientation. WebOrientationLock only |
ANY |
'any' |
Any orientation. WebOrientationLock only |
NATURAL |
'natural' |
The device’s natural orientation. WebOrientationLock only |
UNKNOWN |
'unknown' |
The orientation/lock could not be determined |
WebOrientation only defines the four primary/secondary members (no PORTRAIT/LANDSCAPE/
ANY/NATURAL) — it describes a resolved orientation, not a lock policy.
PlatformOrientationInfo
Section titled “PlatformOrientationInfo”| Field | Type | Description |
|---|---|---|
screenOrientationConstantAndroid |
number | undefined |
An Android ActivityInfo screen-orientation constant. @platform android |
screenOrientationArrayIOS |
Orientation[] | undefined |
The set of allowed Orientation values on iOS. @platform ios |
screenOrientationLockWeb |
WebOrientationLock | undefined |
The lock value to pass to the Screen Orientation Web API. @platform web |
ScreenOrientationInfo
Section titled “ScreenOrientationInfo”| Field | Type | Description |
|---|---|---|
orientation |
Orientation |
The resolved orientation |
verticalSizeClass |
SizeClassIOS | undefined |
The current vertical UIKit size class. @platform ios |
horizontalSizeClass |
SizeClassIOS | undefined |
The current horizontal UIKit size class. @platform ios |
OrientationChangeEvent
Section titled “OrientationChangeEvent”| Field | Type | Description |
|---|---|---|
orientationLock |
OrientationLock |
The orientation lock in effect when the change fired |
orientationInfo |
ScreenOrientationInfo |
The resolved orientation info at the time of the change |
- iOS rejects a lock the device cannot satisfy; Android accepts it.
lockAsyncandlockPlatformAsynccheck the requested mask againstisSupportedByDevice()and throwUnsupportedOrientationLockExceptionotherwise — that is howPORTRAIT_DOWN(andALL, which contains it) fails on an iPhone. Android writes the value straight toactivity.requestedOrientation, and only a lock it cannot map raises at all. Probe withsupportsOrientationLockAsyncbefore locking if you care about the difference. - On Android the lock belongs to the current activity. Every native function resolves
appContext.currentActivityand throwsMissingActivitywhen there is none, and the module writes the activity’s initialrequestedOrientationback when it is destroyed — a lock does not outlive the activity that set it. - iOS re-applies the last lock when the app returns to the foreground. The module unregisters itself while backgrounded and re-asserts the last mask it set on resume; the Android path has no equivalent step.
getOrientationAsyncon Android is best effort. It derives the orientation from the display rotation plus window metrics rather than from a dedicated system API — upstream’s own comment says surface rotation is not standardized across devices. Devices whose natural orientation is landscape take a different rotation-to-orientation branch, so a tablet and a phone held at the same physical angle can report different values.
Common questions
Section titled “Common questions”lockAsyncdoes nothing on tablets and unfolded foldables (Android 16+). Android ignores portrait and landscape locks on screens 600dp or wider; Android 17 removes the opt-out. Design the layout to rotate and resize there.- How do I lock only one screen? Keep the app default orientation unlocked and call
lockAsyncon that screen, thenunlockAsyncwhen leaving. - iPad. Orientation locking on iPad needs the app declared as full screen; without it iPadOS multitasking decides the orientation.
- Initial orientation. Upstream sets it through a config plugin, which this project does not use; set it in the native project instead.
Sources: Expo docs: ScreenOrientation, Android 17 will ignore your portrait lock, Managing orientation changes in React Native.
How the wrapper works
Section titled “How the wrapper works”@symbiote-native/screen-orientation ships zero React/Vue/Angular/Svelte/Solid logic in
expo-screen-orientation itself — its functions and types are hand-ported, verbatim, into this
package’s own core/, resolving the native module through expo-modules-core’s
requireNativeModule rather than the expo meta-package this project never installs:
packages/screen-orientation/src/├── core/ lock*/unlock/get* functions + addOrientationChangeListener│ subscription. native-module.ts resolves the native module through│ expo-modules-core's requireNativeModule. types.ts —│ Orientation/OrientationLock/SizeClassIOS/WebOrientationLock/│ WebOrientation enums, PlatformOrientationInfo, ScreenOrientationInfo,│ OrientationChangeEvent, hand-ported from ScreenOrientation.types.ts,│ plus ScreenOrientationState (this package's own hook/composable/│ service return shape).├── react/hooks/ @symbiote-native/screen-orientation/react — useScreenOrientation├── vue/composables/ @symbiote-native/screen-orientation/vue — useScreenOrientation (same name)├── svelte/runes/ @symbiote-native/screen-orientation/svelte — useScreenOrientation (same name)└── angular/services/ @symbiote-native/screen-orientation/angular — ScreenOrientationService (`.connect()` returns a Signal)Each adapter’s hook/composable/rune/service is a thin lifecycle wrapper — seed from one-shot
getOrientationAsync()/getOrientationLockAsync() calls, subscribe to
addOrientationChangeListener, unsubscribe on unmount — over the same core functions, the same
one-listener seed-then-subscribe shape as network’s
useNetworkState/NetworkStateService. The native code itself is never vendored or copied —
expo-modules-autolinking resolves it straight out of node_modules (see the native setup
guide).