---
title: "UI across orientations"
slug: /workflows/ui-orientation/
markdown: /workflows/ui-orientation.md
section: ui
tier: 2
summary: "One UI tree that works in portrait and landscape."
sources: ['uitk-orientation.md']
canonical: https://unityworkflowlibrary.dev/workflows/ui-orientation/
read_first: /read-first.md
---

> Read first, on every prompt: [non-destructive rules](/read-first.md).

# UI across orientations

> One UI tree that works in portrait and landscape.

For a screen that must work in portrait **and** landscape. Layout behaviour of a built screen is
the [UI Toolkit runtime](/workflows/ui-toolkit-runtime/) page; the USS capability delta is the uss uxml page. This file is only the
orientation axis.

## PLAYER-ORIENTATION-IS-NOT-UI-ORIENTATION (HARD)

Setting `PlayerSettings.defaultInterfaceOrientation = AutoRotation` and enabling all four
`allowedAutorotateTo*` flags **does not make the UI adapt.** It only lets the OS hand the app a
rotated surface; a layout with fixed pixel positions rotates into a broken version of itself.

Orientation support is three separate things, and the first two are the ones agents skip:

1. **USS authored to reflow** — flex directions and percentages, never absolute pixel positions for
   structure. `position: absolute` is for overlays, not for the skeleton.
2. **A direction switch** — one `landscape` class toggled on the document root when
   `Screen.width > Screen.height`, with USS rules that only change *direction and proportion*, never
   content. One UXML tree, two layouts. Duplicating the UXML per orientation is the failure mode:
   the two copies drift and every later change costs double.
3. **The player settings** — which merely permit the rotation.

Minimal switch (the whole mechanism):

```csharp
bool landscapeNow = Screen.width > Screen.height;
if (!_applied || landscapeNow != _isLandscape)
{
    _isLandscape = landscapeNow;
    _applied = true;
    _root.EnableInClassList("landscape", _isLandscape);   // _root = UIDocument.rootVisualElement
}
```

Put it on the `UIDocument`'s GameObject with the document as a `[SerializeField]` — not a
`GetComponent` (the references page). Guard on a change flag: `EnableInClassList` every frame
is wasted style invalidation.

## PANEL-SCALE-MODE-DECIDES-BOTH-ORIENTATIONS

`PanelSettings` defaults to **Constant Pixel Size**, which does not scale with the screen at all — the
single most common reason a UITK screen looks right in the editor and wrong on device.

For dual orientation: `scaleMode = ScaleWithScreenSize`, `screenMatchMode = MatchWidthOrHeight`, and
**`match = 0.5`**. Match 0 keys entirely off width and 1 entirely off height; either makes the *other*
orientation the one that breaks, because the axis it ignores is the one that changed. 0.5 splits the
difference so neither orientation is privileged. Set `match` through `SerializedObject` on `m_Match`
if the C# property is not exposed in your version.

Note the panel's logical size is not the screen resolution once scaling is on — a 1920×1080 screen
against a 1080×1920 reference resolves to a panel of roughly 1641×923. Never assume
`rootVisualElement.resolvedStyle.width == Screen.width`.

## VERIFY-ORIENTATION-BY-RESOLVEDSTYLE-NOT-SCREENSHOT

A screenshot proves something rendered. It does not prove the layout *reacted* — a screen that
ignored the switch and one that handled it can look similarly plausible at a glance.

Assert the resolved geometry at both aspects:

```csharp
root.ClassListContains("landscape")            // the switch fired
table.resolvedStyle.flexDirection              // Column in portrait, Row in landscape
opponents.resolvedStyle.flexDirection          // Row in portrait, Column in landscape
```

Measured on the corpus that produced this rule: portrait 1080×1920 → `table` Column /
`opponents` Row; landscape 1920×1080 → `table` Row / `opponents` Column, from one UXML. That is the
evidence that makes the claim `play-verified` rather than "the screenshot looked fine".

Drive the two aspects from the editor by adding `FixedResolution` entries to the **current build
target group**'s `GameViewSizes` and setting `GameView.selectedSizeIndex` (game-view presets are
per-build-target-group — the [HTML/CSS mock to UI Toolkit](/workflows/html-css-to-ui-toolkit/) page). Set `PlayerSettings.runInBackground = true` first, or
an unfocused editor stops rendering play mode and every capture is a stale frame.

## A NEW PROJECT HAS NO RUNTIME THEME

`PanelSettings.themeStyleSheet` is null in a fresh project, and a panel with no theme renders nothing
— no error, no warning, just an empty screen that reads as "my UXML is wrong".

Create one before the first screen: a `.tss` file whose entire contents are

```css
@import url("unity-theme://default");
```

Unity imports that into a `ThemeStyleSheet` you can assign. Then zero the default theme's label and
button chrome once, or every vertical metric lands ~40% tall (UITK-DEFAULT-THEME-CHROME-RESET,
the [HTML/CSS mock to UI Toolkit](/workflows/html-css-to-ui-toolkit/) page).

## PORTRAIT-ONLY-MATCHES-WIDTH (HARD)

PANEL-SCALE-MODE-DECIDES-BOTH-ORIENTATIONS prescribes `match 0.5` for a UI that must survive both
orientations. **A single-orientation design authored at a fixed width uses `match 0` (width).**
Match 0.5 scales by the geometric mean of the two axis ratios, so on any phone taller than the
reference aspect the logical width comes out BELOW the reference: FOURCE's 450x800 table on a
1440x3120 phone (787x1706 after Fixed DPI 328) resolved to a 405x879 logical panel, and every
element authored at 450 wide overflowed the frame - the hand rows ran off both edges, seats and
buttons crowded. `match 0` gave 450x975: the width fits exactly and the extra height goes to
whichever band is `flex-grow: 1`, which is the pile area on a card table. Rule: the moment the
developer locks an orientation, flip the match to the axis the design was authored on, and read the
logical size back (`panel.visualTree.resolvedStyle`) at the device's post-DPI render size, not
in a free-floating Game view. The invariant behind this AND behind `match 1` for a fixed vertical
stack is PANEL-MATCH-AXIS in the [HTML/CSS mock to UI Toolkit](/workflows/html-css-to-ui-toolkit/) page: match the axis the design was authored on;
`0.5` is only for dual orientation.

## PANELSETTINGS-MUTATION-NEEDS-STATIC-OWNER

`PanelSettings` is ONE asset shared by every panel, and an additive swap loads the incoming scene
BEFORE unloading the outgoing one (ADDITIVE-SWAP-LOAD-THEN-UNLOAD). Per-component capture-in-
`OnEnable` / restore-in-`OnDisable` of `referenceResolution` therefore runs in the order: incoming
applies landscape, outgoing disables and restores portrait over it — landscape silently does
nothing while every USS rule matches. Anything that mutates the shared asset needs process-wide
ownership: a static capture of the original plus a user count; the first panel in captures, only
the LAST one out restores. Verify by reading `panelSettings.referenceResolution` back after the
swap. (a card game.)
