diff --git a/.github/workflows/agent-commands.yml b/.github/workflows/agent-commands.yml
index fd5e1ca9829fe8..1dfb56f5ca7175 100644
--- a/.github/workflows/agent-commands.yml
+++ b/.github/workflows/agent-commands.yml
@@ -17,8 +17,10 @@
# in place instead of posting a new one (GitHub keeps the
# edit history). If the thread has no findings comment
# yet, a new one is posted. Dispatch input: `retry`.
-# ... --announce post the "started — watch the run" comment. Off by
-# default; the eyes reaction is the only receipt.
+# ... --no-announce skip the "started — watch the run" comment; the
+# eyes reaction is then the only receipt. Announcing
+# is the default on the comment path (dispatch runs
+# are always quiet).
# @expo-bot check verify + review from one comment. This workflow runs the
# verify half (--fix / --no-fix apply, issues and PRs);
# expo-code-review-command.yml picks up the review half
@@ -515,12 +517,14 @@ jobs:
;;
esac
fi
- # The "started — watch the run" comment is opt-in: threads stay
- # quiet unless the commander asks for the watch link with
- # --announce. The eyes reaction remains the default receipt.
- announce=false
- if [ "$QUIET" != "true" ]; then
- case " $line " in *" --announce "*) announce=true ;; esac
+ # The "started — watch the run" comment posts by default on the
+ # comment path; --no-announce opts a thread out, leaving the eyes
+ # reaction as the only receipt. Dispatch runs stay quiet always.
+ announce=true
+ if [ "$QUIET" = "true" ]; then
+ announce=false
+ else
+ case " $line " in *" --no-announce "*) announce=false ;; esac
fi
# --retry: the publish phase passes updateLatest to the findings
# tool, so the thread's previous findings comment is edited in
@@ -562,10 +566,10 @@ jobs:
GH_TOKEN: ${{ secrets.EXPO_BOT_GITHUB_TOKEN }}
run: gh api -X POST "repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions" -f content=eyes
- # Opt-in via --announce: the Actions run streams the agent's live
- # progress, and this posts that link the moment work begins (the
+ # Watchable from the start: the Actions run streams the agent's live
+ # progress, so the thread gets that link the moment work begins (the
# findings comment lands separately when the investigation completes).
- # Default is no comment — the eyes reaction is the only receipt.
+ # --no-announce opts out, leaving the eyes reaction as the only receipt.
- name: Announce run
if: steps.gate.outputs.ok == 'true' && steps.cmd.outputs.route == 'true' && steps.cmd.outputs.announce == 'true' && env.QUIET != 'true'
env:
@@ -588,9 +592,9 @@ jobs:
# are in_progress too, so ordering by (created_at, id) IS the queue;
# admission by global position keeps the executing count at CAP even
# when several waiters poll at once. Runs pending on the per-target
- # group are not in_progress and hold no capacity. With --announce the
- # thread has the watch link during this wait; by default the queue
- # wait is visible only from the Actions tab.
+ # group are not in_progress and hold no capacity. The announce above
+ # already posted the watch link (unless --no-announce), so the queue
+ # wait is visible from the thread.
- name: Wait for a free verification slot
if: steps.gate.outputs.ok == 'true' && steps.cmd.outputs.route == 'true'
env:
diff --git a/0380-expo-runtime-agent-semantics.rfc.md b/0380-expo-runtime-agent-semantics.rfc.md
new file mode 100644
index 00000000000000..7e63408c7276dc
--- /dev/null
+++ b/0380-expo-runtime-agent-semantics.rfc.md
@@ -0,0 +1,1825 @@
+# LLP 0380: The Runtime-Owned Agent Semantics Layer Expo Should Build
+
+**Type:** RFC
+**Status:** Review
+**Systems:** Cross-pollination (Expo/React Native), Acto/Agent API, Contract (verification model), Verification, Process
+**Author:** Charlie Cheever / Claude (Fable 5)
+**Date:** 2026-07-21
+**Revised:** 2026-07-22 (rev 23 — round-22 review response; full history in the Revision History appendix)
+**Related:** `llp/agent-feedback-system.rfc.md` (the original agent-inspector RFC — Acto's actual origin; runtime-owned from the start), LLP 0032 (direct agent transport/compound execution — an Acto refactor, not its origin), LLP 0321 (Acto agent subsystem v2 — the mature shape this distills; Workstream E's host/device provider seam is the *pattern* precedent for §3.1's semantics-provider extension — semantic-subtree providers themselves are new work), LLP 0374 (Acto after Partitime — the evidence-facet model §3.0 adopts), LLP 0320 (agent API competitive comparison — the outside-in tool analysis behind §2.2), LLP 0326 (Acto cross-tool benchmark — pinned-run economics evidence behind §3.2), LLP 0365 (agent-first guide — the capability-discovery lesson behind §3.5), LLP 0085 (contract blocks — the claims model §3.4 ports), LLP 0278 (contract verification depth — the claims-tier design; §3.4's baseline is stated against current code, which has since landed the live clause forms 0278 planned: `change` outcomes, expression-valued counts, guarded `when`, runtime action-writes), LLP 0136 (Exact vs React Native rendering — the Fabric/Fiber/host-registry architecture §3.1 must join), LLP 0376 (cross-domain lessons *into* Exact — this document is the outbound sibling)
+
+> **Scope note:** this RFC proposes work in the **Expo/React Native** product,
+> not in Exact. It lives in this corpus because the design is distilled from
+> Exact's Acto subsystem and the author co-owns both contexts. Nothing here
+> changes Exact behavior; Exact appears only as the reference implementation
+> of the pattern. Precedent for outbound/comparative docs: LLP 0136, LLP 0339
+> (both Research; this is the corpus's first proposal directed at another
+> organization's product, and the disclaimers here carry that weight).
+> Because acceptance criteria live in another organization, this document
+> uses LLP 0001's status vocabulary, with `Review` as the closest
+> available resting status for a document whose acceptance authority is
+> external: it sits at `Draft` until its review loop completes, then
+> rests at `Review` — the author's position carried into Expo planning; it moves to `Accepted` only if a **named Expo
+> decision authority approves a precisely scoped program**, with that
+> decision artifact linked here (the author alone never advances it);
+> it can never become `Implemented` in LLP 0001's sense, since nothing
+> here is implemented in this codebase; once an Expo-owned authority
+> replaces it, it moves to `Superseded`, and on rejection with no
+> replacement artifact, `Withdrawn`.
+>
+> **Decision requested:** the immediate ask is authorization to
+> **prepare the M-1 brief** — the populated artifact with numeric
+> staff-week and calendar ceilings (per-item and aggregate, with the
+> apparatus itself a line item and apparatus spend explicitly excluded
+> from any continuation argument), sampling frame, kill/continue
+> thresholds, the named sponsor, custodian, and adjudicator, **and a
+> mandatory corpus-governance plan** for the sensitive data M-1
+> collects (ecological Expo Agent traces, field incidents): defaulting
+> to synthetic/internal or expressly authorized traces, and specifying
+> source authority, consent/eligibility basis, minimization,
+> de-identification and secrets scanning, raw-data access and audit
+> logging, retention/deletion, partner-export rules, and
+> conflict/recusal handling — with a CEO sponsor, custody and
+> adjudication are external or carry enforceable operational
+> independence, not just a reporting-chain clause. **M-1 execution
+> requires a second approval of that brief**, which also checks the
+> M-1 short-form pitch for claim-strength fidelity against this
+> document (no pitch statement stronger than its §3.7 provenance row); M0 requires M-1's result; M1–M4 are contingent layers, each
+> requiring fresh approval per the §4 spike matrix. Nothing in this
+> document's status ever implicitly endorses the full program.
+
+## 1. Summary
+
+The React Native ecosystem has converged, fast, on agent-driven app
+development: Expo ships an MCP server (docs, EAS, stores, plus a thin local
+automation tier), Software Mansion ships Argent (control/debug/profile a
+running app over MCP), Callstack ships agent-device (CLI inspect–act–verify
+with accessibility-tree snapshots, element refs, diffs, and assertions), and
+Expo Agent — the end-to-end app-building product — consumes all of it.
+
+These tools observe the app **from outside the renderer's contract**: the
+accessibility tree, XCTest/ADB, and devtools protocols. Some do reach React
+internals (component trees, profiler data) through devtools channels, but
+none has a supported, versioned semantics contract from the renderer itself.
+Exact's experience building Acto says the ceiling of the outside-in approach
+is structural, not incremental — and it also says something the naive
+version of this proposal misses: a runtime-owned tree has its *own*
+signature failure mode (certifying interactions no real user could perform)
+unless input-delivery evidence is modeled as a first-class, separately-owned
+fact.
+
+This RFC therefore proposes that Expo build the inside half as a
+**provenance-bearing evidence protocol** (working name: **RASP — Runtime
+Agent Semantics Protocol**; the acronym collides with mobile security's
+Runtime Application Self-Protection, so choosing a real name — candidates:
+"Runtime Semantics Protocol (RSP)," "Expo Semantics Layer" — is an M0 exit
+requirement, not a someday):
+runtime semantics, renderer layout, native reachability, OS-driver-delivered
+input, causal attribution, and claim execution are distinct facts,
+contributed by distinct authorities (the runtime, the renderer, the native
+host, and external drivers), each carried in an evidence envelope that
+declares what was witnessed, by whom, about which incarnation of the app,
+and how it was joined to the action under verification. Expo MCP, Argent,
+agent-device, and Expo Agent all consume it; external drivers also
+*contribute* to it (input-delivery facts) through a correlation protocol.
+Expo's edge here is not literal exclusivity — it is that Expo controls the Expo-managed distribution channel (Expo
+Go, dev clients, SDK releases — not the RN renderer itself) and
+already absorbs RN version churn for the ecosystem, which is exactly the
+maintenance burden that keeps outside-in tools outside. And the bet is
+layered, not all-or-nothing: the protocol has a named **minimum viable
+core** — host semantics, identity, opaque-region reporting, layout and
+native-reachability facts, attestation, conformance (§4) — worth building
+even if the hard layers (Fiber provenance, causal attribution, claims,
+release certification) resolve pessimistically.
+
+## 2. Motivation
+
+### 2.1 The landscape (as of 2026-07)
+
+| Surface | Owner | What it does | Observation source |
+| --- | --- | --- | --- |
+| Expo MCP server | Expo | Docs/EAS/store tools + 5 local automation tools (tap, screenshot, find_view, logs, router sitemap); iOS sim on macOS only | Simulator tooling |
+| Argent | Software Mansion | Control (gestures, text, deep links), debug (logs, view hierarchies, React component tree, network payloads), profile (React↔native correlation); platform scope uncertain at retrieval — the docs page said sims/emulators while LLP 0320's earlier source notes recorded Android physical-device support in its README; resolved by the pinned M-1 record | a11y tree + devtools protocols |
+| agent-device | Callstack | inspect–act–verify loop: a11y snapshots with `@eN` refs, action diffs, assertions, wait-and-settle, evidence capture, `.ad` replay / Maestro export; iOS/tvOS/Android/web/desktop | a11y tree (XCTest/ADB) + RN component tree |
+| Expo Agent | Expo | End-to-end "idea → shipped app" product | All of the above (author's understanding as of 2026-07; validated at M-1/M0 like the rest of this table) |
+
+Source notes: rows retrieved 2026-07-21 from docs.expo.dev/mcp,
+docs.expo.dev/agents/argent, github.com/software-mansion/argent,
+github.com/callstack/agent-device, docs.expo.dev/agents, and
+expo.dev/blog/expo-agent-beta; tool versions were not pinned at
+retrieval and these positioning claims will age quickly — the sources
+are pinned and archived with the M-1 pitch (not deferred to M-1
+execution), and M0 re-validates the table with partners directly. The table is
+load-bearing for *positioning* (§3.5, M0/M4) but not for the *ceiling
+argument* (§2.2), which is architectural; modest differences in any row do
+not change the proposal. The one external assertion the proposal genuinely
+rests on is narrow — *no current tool exposes a supported, versioned
+renderer semantics contract* — and its validation is scheduled once,
+consistently: M-1 entrance diligence performs the archived-source check
+and renderer-owner inventory, M0 revalidates with written partner
+confirmation, and discovery of an existing contract — even partial —
+automatically reframes the work as interoperability *before* any
+thesis-specific build arm is approved.
+
+The outside half of the loop is being built well, by Expo's two closest
+partner consultancies. Duplicating it would be redundant. The inside
+half — a supported semantics contract from the renderer — is unowned:
+the React DevTools backend protocol is the closest existing thing, and
+it fails the bar on every axis that matters here (version-skew history,
+human-tooling orientation, no layout/reachability/receipt facts, no
+compatibility promise).
+
+### 2.2 Why outside-in has a ceiling
+
+These lessons come from Acto's design record and from Exact's benchmarking
+of accessibility-tree-based agent tools (LLP 0320's competitive comparison;
+LLP 0326's pinned cross-tool runs). Acto was runtime-owned from its first
+RFC (`llp/agent-feedback-system.rfc.md`), with accessibility as one
+projection of the runtime tree among several — the comparison below is with
+the outside-in tools Exact benchmarked against, not with an abandoned Acto
+design.
+
+1. **Lossiness.** Accessibility trees drop nodes with no a11y
+ representation, and the platform paths for developer handles differ
+ (iOS `accessibilityIdentifier` vs Android's view-tag/resource-id
+ routes), so handle coverage is inconsistent across platforms. Agents
+ cannot reason about what they cannot see, and — worse — cannot know what
+ they aren't seeing.
+2. **Snapshot refs need a cross-snapshot identity behind them.** An a11y
+ snapshot ref describes what was visible at capture time. Acto's answer
+ is two-tier: refs are deliberately snapshot-local, and a renderer-owned
+ `viewId` provides identity *across* snapshots for scrolling and
+ re-render. Virtualized/recycled cells remain an open identity problem
+ even for Exact (LLP 0321 OQ10: a recycled id means "this slot," not
+ "this item") — §3.1 and §6 treat identity as a design problem to solve,
+ not a solved import.
+3. **Causal observation needs runtime cooperation.** After an action, the
+ agent wants to know what changed and why. Outside-in tools can diff
+ successive snapshots; correlating changes to the action that caused them
+ — and distinguishing them from ambient re-renders — requires the
+ renderer's commit pipeline (§3.2 for what this does and does not
+ promise).
+4. **Layout diagnostics need the layout engine.** *Why is this clipped /
+ zero-size / behind that view* is answered from renderer layout state
+ for renderer-owned clipping, sizing, and stacking — with native-host
+ evidence for higher windows and host chrome (see item 5 and §3.3). The switcher-clicks bug class from Exact is the canonical
+ example: every click on a visible, enabled control silently swallowed
+ by a native overlay above it (a topmost highlight view claimed the
+ hit) while every semantic check passed. It was not
+ authoritatively diagnosable from accessibility/semantic snapshots
+ alone, was *undetectable* by the semantic-mode tooling in use at the
+ time (the LLP 0326 benchmark ran other tasks; the incident itself
+ was not a benchmarked run), and its symptom was visible to real
+ input all along (see item 5).
+ Note the converse in item 5: renderer-layout facts alone are not
+ input-delivery truth.
+5. **Runtime-owned trees have their own false-positive class.** Exact's
+ most expensive recent Acto lesson (the LLP 0321 Partitime boundary; LLP
+ 0374 exists to systematize it): an action can dispatch successfully in
+ the semantic tree while host chrome, a higher native window, a system
+ alert, or the keyboard makes the target unreachable by real input. "A
+ press that only passes in semantic mode is suspect"
+ (docs/acto-guide.md). Acto's remedy — `nativeReachability` evidence on
+ receipts, native-window hit testing, an escalation ladder toward
+ realistic input — is a design input to §3.0/§3.2/§3.3, and it is also
+ why partners stay essential: XCTest/ADB drivers are where
+ OS-delivered-input truth lives.
+6. **Claims need semantics.** A verification layer ("this button exists and
+ dispatches this action") can only be checked against a tree with stable
+ identity and event-binding information.
+
+### 2.3 Why Expo, why now
+
+- **Expo Agent's reliability ceiling plausibly sits at this gap** — but
+ that is a hypothesis this proposal budgets to test, not a premise: M-1
+ measures the current failure distribution before anything is built. An
+ agent that builds apps end-to-end is only as trustworthy as its ability
+ to verify its own work — including *not* being fooled by semantic-only
+ success (§2.2 item 5).
+- **Standardization window.** Three vendors are shipping overlapping
+ observation layers. Publishing the runtime-side protocol first can establish an early
+ coordination point (whether it becomes the schema others map to is a
+ governance and adoption outcome, not an automatic consequence). Expo's advantage is distributional
+ and economic, not exclusive: shipping inside the renderer requires
+ absorbing RN version churn, which Expo already does at ecosystem scale.
+- **Exact has run most of this experiment.** Acto is the existence proof
+ for the snapshot economy, receipts, diagnostics, and the discovery
+ surface — exercised daily by agents building real UI
+ (author-reported operational experience; the repository proves the
+ capabilities, not the usage frequency) — and Contract's
+ claims machinery is further along than its early docs suggest (see the
+ mechanism matrix in §3.4). It is *not* proof of everything here: the
+ observation-economics results are mixed (§3.2), and the Fabric/Fiber
+ joins and attribution under concurrent React are new work. §3.7 grades
+ every proposed capability by its actual provenance.
+
+## 3. Proposal
+
+Build the following in the Expo runtime, exposed as one versioned logical
+schema, delivered dev-mode-first.
+
+### 3.0 Design stance: evidence, not omniscience
+
+RASP is a **provenance-bearing evidence protocol**. Distinct facts come
+from distinct authorities and are never conflated:
+
+| Fact | Authority |
+| --- | --- |
+| Target identity & artifact attestation (project, artifact digest, runtime version, effective configuration, process epoch) | Dev server + host bootstrap (attestation protocol) |
+| Semantic structure & dispatch | JS runtime / renderer bindings |
+| Committed layout geometry, clipping, stacking | Fabric/Yoga (shadow-layout intent) |
+| Mounted native geometry, native-driven scroll/animation | Native host view registry |
+| Native reachability (would an input at this point land?) | Native host (window-level hit test) |
+| Runtime-witnessed native ingress | Native host / runtime (in-process observation) |
+| In-process realistic input delivery (responder-path, identity-minting) | Runtime/host tier (§3.2) |
+| OS-driver-delivered input | External driver (XCTest/ADB — partner-owned), via correlation protocol |
+| Causal attribution (this commit ← this action) | Renderer commit pipeline, where propagated |
+| Router state & transitions | Expo Router (settled route, transition records) |
+| Claim execution | Claims runner over the above |
+
+**Verification is bound to an attested target, at a stated evidence
+strength — and for Expo, the target is compositional.** An Expo app's
+identity is not one digest: native build identity, embedded *and
+activated* EAS Update identity with its JS hash, the asset graph,
+effective config/plugin inputs, runtime compatibility, and the
+development module/HMR epoch all change independently, so the attested
+tuple is a **compositional manifest with per-component invalidation
+rules** (an OTA activation or rollback invalidates exactly what it
+changes), with activation, rollback, mixed-update, and asset-mismatch
+fixtures alongside the ones below. And the trust boundary is stated,
+not implied: `measured`/`signed` evidence detects **mismatch among
+trusted participants** — it is not device-integrity or tamper
+attestation, and claims never assert what only an independent attester
+could prove. A correct receipt from the wrong project, a stale bundle, a
+differently-configured build, or a surviving old process is still a false
+pass — the expectation-binding lesson of LLP 0374 W1. Every claim and
+receipt binds to the attested tuple (project, artifact digest, runtime
+version, effective configuration, process epoch); verification that
+requires target identity **fails closed** on mismatch or unknown. And the
+binding itself is graded: each attested field carries an
+**attestation-evidence class** — `self-declared` | `compared` |
+`measured` | `signed` (0374 W1's ladder) — plus its authority, freshness,
+and invalidation rule, because a process that *self-reports* the expected
+digest satisfies the tuple syntactically without proving which bytes it
+loaded. Certification profiles (§3.4) name the minimum class per field;
+claims about loaded bytes and effective configuration require `measured`
+or `signed`. Conformance fixtures: wrong-project, stale-bundle,
+same-revision/different-config, mixed-HMR state, process restart,
+dev-channel-vs-release-artifact, and **expected-manifest-self-reported
+while running different bytes** — each must fail, not pass. The M3
+tethered release-configuration verification artifact (§3.5) is this
+same binding applied to the release-configuration sibling.
+
+The **source-native facts and their join edges are the authoritative
+record**; any convenient fused tree is a projection over that graph, never
+a replacement for it — a projection must not erase which authority
+supplied each field. Fields carrying on-screen content additionally carry
+**content provenance**, adopting LLP 0271's **normative model in
+full** (what Exact *ships* today is the taxonomy, the hostile-ordering
+helper, and a hand-annotated tree slice — taint inference and per-span
+provenance are explicitly deferred there, and several surfaces still
+lack provenance; the model is the import, the coverage is new work): **separate source-authority
+and authorship/intent axes**, the complete class vocabulary (`app` |
+`system` | `user` | `other-user` | `third-party` | `external` |
+`generated` | `data-unknown`), and an **effective class derived
+conservatively** — when the axes disagree, the more hostile result
+wins. Disclosure/redaction remains a further independent axis (per LLP 0374
+W5).
+Unclassified dynamic content defaults to `data-unknown` and is treated
+as hostile (only statically-known literals may default to `app` —
+incorrect provenance is worse than none); each stronger classification
+names its authority; annotation/taint propagation, loss semantics, and
+laundering fixtures — including the `user`/`third-party`/`external`
+classes and conflicting-axes cases — must exist before provenance
+informs any prompt-injection decision. And the 0271 invariant is
+normative here too: **provenance is advisory; it can never authorize
+effects or relax capability/consequence policy** — capability
+confinement is the load-bearing layer, provenance the label on top.
+
+**The evidence envelope.** Every fact carries, as separate axes:
+
+- **domain outcome** — the producer's own verdict in its own vocabulary
+ (dispatched / no-handler; reachable / blocked-by(x); delivered /
+ rejected; committed / discarded). A witnessed negative ("the native hop
+ was rejected") is evidence, not absence of evidence;
+- **evidence availability** — `witnessed` | `degraded` | `absent`, with a
+ separate **reason class** preserving why (`unavailable`, `unsupported`,
+ `unobserved`, `evicted`, `timed-out`, `redacted`, `denied`, …). The two
+ axes are deliberately not collapsed: an evicted expected fact is not a
+ fact never produced, and redacted evidence can be present-but-degraded
+ rather than absent. (This matches the crosswalk Exact actually ships —
+ `packages/exact-devtools/src/agent/conditions/status-crosswalk.ts` —
+ and LLP 0374's availability/reason separation.);
+- **causal join method** — how this fact was tied to the action under
+ discussion: `ingress-propagated` | `exact-key` | `armed-bracket` |
+ `heuristic` | `none` (§3.2);
+- **producer identity and native status** — which authority produced it and
+ that producer's health;
+- **producer incarnation and source revision** — which app run / root /
+ commit revision the fact describes;
+- **freshness and coordinate space** — capture time/skew, and the space
+ any geometry is expressed in;
+- **disclosure/retention** — whether the fact may be displayed,
+ persisted, replayed, or exported to partners, composed
+ most-restrictive-wins across wire projections, reports, replay,
+ screenshots, and contributed evidence (a fact can be fully witnessed
+ yet forbidden from display — `redacted`-as-availability-reason does
+ not carry this).
+
+Facts a verification *expects* but did not receive appear as explicit
+obligations ("native reachability: obligation unmet"), never as silent
+omissions. Heuristic joins never satisfy verification; they inform
+exploration only. An **acceptance truth table** — which
+(outcome, availability, reason, join) combinations may satisfy which
+verification obligations — is part of the schema, with conformance
+fixtures proving that degraded, evicted, timed-out, and redacted evidence
+cannot accidentally satisfy a required check. The envelope, truth table,
+and fixture manifests are published at M0 as a small normative protocol
+appendix, separate from this document's strategic narrative.
+
+**Snapshot coherence.** A "bundled snapshot" is a *bounded-skew cut*
+across sources that tick independently — React commit, Fabric mount
+transaction, native window state, screenshot capture — not an atomic
+freeze. Each bundle declares the per-source revision/timestamp it captured,
+and consumers classify the cut as `coherent` (revisions align), `skewed`
+(bounded, declared skew), `contradictory` (sources disagree about the same
+fact — a *useful result*, surfaced as such, not normalized away), or
+`unavailable`. M0's fixtures must include mount lag, native-driven
+transforms/scroll, higher windows, and screenshots racing commits.
+
+The core projection is **host-semantic** (what exists, where, with what
+handles and bindings); React component provenance (component names,
+allowlisted props, fiber ancestry) is an optional extension, not a
+requirement for one globally fused tree.
+
+This stance is the direct import of LLP 0374's evidence-facet model, and it
+is what prevents RASP from institutionalizing the false-positive class in
+§2.2 item 5.
+
+### 3.1 Semantics tree with a snapshot economy
+
+A runtime-owned tree correlating three in-process representations — none
+of which is exposed through a supported Expo join today: the Fabric
+shadow tree (renderer identity and committed layout intent — mounted
+native view identity is a *separate* fact, joined from the native view
+registry through tags/families, not read off the shadow tree), the React
+component tree (provenance extension), and declared handles (testID,
+accessibility label/role). The joins are genuinely hard — composites map to
+zero or many host nodes, portals cross roots, native-view flattening
+changes topology, fibers are internal — which is why M0 is an architecture
+spike (§4), not a schema freeze.
+
+Surface (names indicative, mirroring Acto):
+
+- `snapshot()` → `{ snapshotId, tree }` — one bounded-skew bundle (§3.0);
+ **every node carries a renderer-owned semantic node id**; addressable
+ nodes additionally carry a **snapshot-local ref**. Nodes expose type,
+ testID, a11y label/role, layout rect (with coordinate space), visibility
+ and clipping flags, and which events they handle.
+- **Identity tiers**, kept distinct (the two-tier Acto lesson, §2.2 item 2):
+
+ | Identity | Owner | Scope & lifecycle |
+ | --- | --- | --- |
+ | `ref` + `snapshotId` | protocol | snapshot-local handle for addressable nodes |
+ | semantic node id | renderer | on every node; stable across commits within a root incarnation; survives re-render; the cross-snapshot correlation key; carried as `{semanticId, lastSeenFingerprint}` so virtualized-slot reuse and handler/trust changes fail closed even for callers that bypass snapshot refs; behavior under keyed remount/recycling defined by M0 conformance cases |
+ | `testID` / a11y label | author | human-declared handles; scoping per §3.5 |
+ | native view tag | platform | debugging/join key only — remounting, flattening, and recycling may change it; never an agent-facing identity |
+ | component instance provenance | provenance extension | optional |
+
+ M0 conformance cases must cover sibling reordering, keyed remounts,
+ Suspense/transitions, portals, native-view flattening, and
+ virtualized-cell recycling ("this slot" vs "this item" — the open problem
+ Exact shares, §6).
+- **Targeting ladder** (for live, in-session interaction), in order:
+ `ref + snapshotId` → semantic node id → `testID` → a11y label →
+ coordinates (last resort). Acto's *documented targeting preference* is
+ `ref → testId → label → viewId → coordinates`; placing the
+ renderer-owned id *above* author handles is a deliberate RASP
+ adaptation, justified by the RN context: author-handle coverage is
+ exactly what §3.5 is still trying to establish by convention, while the
+ semantic id is machine-stable and universally present, so it should
+ outrank handles that may be missing or duplicated. The ladder is
+ strictly session-scoped: the semantic id does not survive an app
+ restart or rebuild, so **cross-run artifacts — replay recordings,
+ claims (§3.4), codegen — target author handles or claim-local handles,
+ never semantic ids.**
+- **Staleness semantics** (adapted from Acto's shipped design): a ref
+ whose target has *changed* since its snapshot fails loudly and
+ instructs a refresh; a ref whose target is *verifiably unchanged*
+ resolves, with the resolution mode carried on the receipt. (In Exact,
+ `clean` refs resolve silently and only `stale-verified` emits a
+ resolution receipt — and the whole graded path is itself flag-gated
+ behind the incremental-serialization default; RASP receipting `clean`
+ as well is new behavior, chosen so verification consumers never have
+ to infer why a ref resolved.) Never a silent rebind.
+ **Target-equivalence is a normative fingerprint, not an event-kind
+ check:** Exact's current stale comparison records the *set of event
+ kinds*, never handler identity, so a handler swap that keeps an
+ `onPress` binding can stale-verify there. RASP's fingerprint starts
+ from Exact's shipped staleness fact list (element type/original tag —
+ the field Exact calls "role" — testId, label, disabled/inert,
+ interactable posture, event-kind signature: the auditable baseline) and must extend it to semantic/owner identity,
+ action-token/binding identity, trust/consequence posture, and
+ native-reachability class — with handler-swap, token-swap, remount,
+ trust-change, reachability-change, and coordinate-path fixtures. Until
+ a deployment produces those fingerprint facts, stale refs **reject**
+ rather than auto-resolve. Subtree-precise staleness (an ambient
+ commit elsewhere must not orphan refs on an unrelated subtree) is the
+ design goal; in Exact it is implemented but flag-gated behind the
+ incremental-serialization default (LLP 0321 C1/B1), so RASP should treat
+ it as a design requirement with partial precedent, not daily-proven.
+- `screenshot(annotate: true)` — refs overlaid on the image, with the
+ bundle's coherence classification attached (a screenshot racing a commit
+ is `skewed`, and says so).
+- Non-Fabric regions (interop views, native screens, WebViews, brownfield)
+ are flagged per-region as `opaque` with bounds — the lossiness criterion
+ (§2.2 item 1) applied to RASP itself: agents must know what they aren't
+ seeing. A **semantics-provider extension** (the LLP 0321 Workstream E
+ provider-seam pattern) lets custom native components, brownfield
+ regions, and WebViews contribute bounded semantic/layout/reachability
+ facts under declared producer identities and evidence ceilings;
+ `opaque` remains the safe default for anything without a provider.
+
+### 3.2 Action receipts
+
+Every runtime-mediated action returns a **receipt** composed of separable
+facts, each carried in the §3.0 envelope (domain outcome + availability +
+join method + producer + incarnation + freshness):
+
+- input delivery — for runtime-synthesized input, the runtime's own
+ dispatch; for OS-driver-delivered input, the driver's witnessed delivery
+ fact joined via the correlation protocol below;
+- semantic dispatch — which handler fired, by opaque action token (§3.4);
+- native reachability at the target point, captured immediately before
+ dispatch against the pre-action state (§3.3) — the
+ fact that distinguishes "dispatched in JS" from "an input delivered at
+ this point would have reached the target." `unavailable` means
+ *unknown*, never *reachable*;
+- correlated commits and the resulting tree diff, where the renderer can
+ propagate an action identity through its commit pipeline; ambient
+ commits (animations, timers, polling — pervasive in RN) are the reason
+ heuristic temporal correlation never satisfies verification;
+- UI quiescence (bounded settle), reported separately from any data/domain
+ convergence, which RASP does not claim to observe.
+
+**Correlation grades.** A causal join is `ingress-propagated` only when
+the action identity is bound at input ingress and carried through the
+pipeline; `exact-key` means the fact was joined on a unique platform/driver
+event key both sides witnessed. **Only these two grades are
+verification-grade — with no exceptions.** A prepare/arm protocol — an
+expiring, single-use nonce that brackets one driver-delivered input —
+yields `armed-bracket`, which never satisfies verification: bracketing
+establishes at most uniqueness among *observed* ingress, not the identity
+of the sole observed event. (The counterexample that kills lease-hardening:
+the driver's event is dropped or diverted while exactly one ambient,
+human, or delayed event arrives inside the bracket — an "uncontested"
+lease then joins the wrong event to the nonce. Exact's own exclusive
+gesture lease only supersedes other agent sessions on a root; it cannot
+exclude human or OS ingress, and no app-side mechanism can.) An
+**exclusive-input lease** remains useful as a *diagnostic* hardening —
+observed competing ingress still forces `unresolvable`, and geometry/
+timing consistency between the driver's delivery fact and the observed
+ingress is reported — but it upgrades nothing. Deterministic conformance
+cases must include "intended event dropped + sole ambient substitute" and
+"delayed prior event is sole ingress," both yielding `unresolvable`.
+Temporal inference is `heuristic`. (A quantified-error-bound tier —
+bracketing with measured ambient-event rates — was considered and
+rejected as a verification grade: the sole-event-substitution
+counterexample is about *identity*, not frequency, and no measured rate
+distinguishes the wrong sole event from the right one.) Whether XCTest/ADB paths can carry
+true ingress identity or a shared exact event key is an open question
+(§6); the protocol is honest at whichever grade a platform achieves, and
+claim kinds (§3.4) say which grades they accept. Receipts also preserve
+input-fidelity detail (requested mode, actual mode, serving authority —
+the LLP 0306 / evidence-adapter vocabulary; these concepts ship in Exact
+but are not one uniform tap-result shape today): semantic vs
+OS-delivered is too coarse for diagnosis.
+
+**The in-process realistic tier.** RASP includes a runtime/host-owned
+**in-process responder-path delivery tier**: input synthesized inside the
+app process at the platform event boundary, exercising real responder
+routing and gesture arbitration without OS-level injection or device
+automation. The shipped precedent is Apple-only — Acto's LLP 0306 P1
+tier, `UIWindow.sendEvent` on iOS and `NSWindow.sendEvent` on macOS; an
+Android `MotionEvent` dispatch analog is *proposed RN-specific work*, not
+precedent. Two honesty rules govern this tier:
+
+- **Minting is not propagation.** The in-process synthesizer can mint an
+ identity for the event it intends to deliver; that identifies the
+ *intended* input, not the one that arrived at the handler.
+ Verification-grade (`ingress-propagated`) status requires *proving*, per
+ platform, that the same identity is bound to the concrete native event
+ and observed at the intended action token after native dispatch,
+ gesture arbitration, responder routing, and RN event conversion —
+ with ambient, concurrent, dropped, diverted, and delayed-input
+ conformance cases. (Acto does not fully supply this either: ingress-identity minting
+ has landed on its *semantic* dispatch path only (the event-trace
+ operation mints it; an ordinary semantic tap does not), while the realistic
+ path still correlates via trace baselining — and what LLP 0374
+ explicitly flags as new owner work is causal `commitId` minting; the
+ end-to-end realistic-event binding is simply not built anywhere.)
+ Until a platform carries that proof, its in-process joins grade
+ `heuristic` or `unresolvable`, and **the non-driver `responderOperable`
+ path is disabled on that platform.**
+- **Known gaps are declared, per tier.** In LLP 0306's table form:
+
+ | Mode | What it proves | Known gap |
+ | --- | --- | --- |
+ | Semantic dispatch | Handler wiring | Everything physical |
+ | In-process responder delivery | Responder routing, gesture arbitration, handler observation inside the app | OS event taps, Accessibility/TCC, cross-process window ordering — anything outside the app's own windows |
+ | OS-driver delivery | Device-level input reaches the app | Correlation grade (§ above); driver cannot see in-app handler outcome without the runtime join |
+
+This does not touch §3.6's partner boundary: OS-level and device-level
+input synthesis (hardware events, system surfaces, other apps) remains
+partner-owned; the in-process tier can only reach the app's own windows,
+and that limit is exactly its declared gap.
+
+**Contributed-evidence trust model.** External drivers are contributors,
+not oracles: contributor principals are authenticated; facts are bound to
+an app/device/run channel; nonces are single-use and expiring; receipts are
+replay-protected; and each contributor has an **evidence ceiling** — a
+driver cannot upgrade its own facts to a grade the runtime did not
+witness. Adversarial conformance cases (concurrent input, forged and
+duplicated receipts, delayed delivery, wrong-target, driver crash, ambient
+input) must exist before M3 consumes contributed evidence.
+
+**Economics honesty:** receipts are proposed here for *reliability* — an
+agent that reads "dispatched, but reachability obligation unmet" does not
+ship a broken flow. Exact's evidence on the *token economics* of
+diff-first observation is mixed: diff-first remains opt-in in Acto because
+paired model runs used more total and uncached input in the diff-first arm
+(LLP 0326; LLP 0321 F2), and completion rate — not token count — is the
+metric that matters. M2 reports observation-economics numbers but does not
+gate on them (§4). What concurrent React does to "the commits this action
+caused" (transitions, deferred updates) is open (§6); receipts ship with
+the correlation grades above precisely so the answer can be honest
+per-fact. If attribution degrades to `heuristic` on most real apps, the
+layer remains differentiated on coverage-explicit structure, native reachability,
+and diagnostics alone — none of §2.2 items 1, 4, or 5 depends on
+attribution.
+
+### 3.3 Layout and interaction diagnostics
+
+Structured queries answered from the renderer and the native host, each
+answer labeled with its authority and envelope (§3.0):
+
+- `why_invisible(ref)` — clipped by which ancestor, zero-size from which
+ constraint, opacity/display, off-viewport (committed-layout authority;
+ mounted-native geometry consulted where native-driven animation/scroll
+ can diverge from committed intent).
+- `hit_test(x, y)` / `why_not_hittable(ref)` — the layout-tree hit path
+ *and* a native-host probe over the window/surface classes the app
+ process can see (native authority; the Partitime lesson). **Every
+ reachability fact declares its per-platform enumeration scope** —
+ which window and surface classes the probe could observe (own windows
+ and keyboard visibility are platform-contingent M0-produced scope
+ facts, not cross-platform guarantees; cross-process system surfaces
+ such as permission dialogs: not enumerable on iOS) — and occlusion classes *outside* that
+ scope are reported as an explicit unverifiable residual on the verdict,
+ never silently absorbed into `reachable`. (Exact's shipped probe is
+ likewise bounded — in-process and therefore same-app by construction,
+ with the iOS bridge enumerating only strictly *higher* window levels
+ in the current scene — and its tap receipts report reachability facts
+ rather than this full machine-readable enumeration residual, which is
+ new RASP behavior; same-level *other-window* and cross-process occlusion are residuals
+ there too (same-window hierarchy is inspected; the iOS bridge
+ enumerates only other windows at strictly higher levels), and the iOS realistic-input precedent is
+ DEBUG-only via private UIKit hooks, so distribution legality is its
+ own residual. The scope declaration is what keeps such limits
+ honest.) Where the probe
+ is unavailable, the answer is `unavailable` — unknown, never presented
+ as reachable.
+- `resolve_target(ref)` — recommended interaction point and which
+ stacking/scroll ancestor is responsible for the current occlusion state;
+ in Acto these were the highest-leverage diagnostic outputs in
+ practice (author-reported operational experience).
+- `stacking(ref)`, `safe_area()`, layout-constraint dumps for a subtree.
+- Verdict diagnostics are generated from the proof graph as
+ **minimal-cut explanations**: the smallest missing or contradictory
+ evidence set preventing a claim from passing — the evidence algebra
+ doing double duty as the agent's repair hint. The same graph drives a
+ **proof-directed acquisition planner**: request the cheapest
+ authoritative facts first, escalating to native probes or external
+ drivers only when the unresolved minimal cut requires them — bounding
+ observer effect and driver cost by construction. Authoritative fact
+ *production* stays in renderer/runtime code; graph fusion, ledgers,
+ report generation, and claims planning live in dev-server tooling
+ unless runtime ownership is required, limiting renderer-version
+ coupling.
+
+### 3.4 Declared claims (v2 of the layer; v1 of the syntax is deliberately small)
+
+Port the contract-block *verification* model (LLP 0085) without the
+Contract notation and without overpromising. v1 claims are limited to what
+the evidence layer can actually check:
+
+```ts
+// v1 primary form: route-scoped — no React component identity involved
+routeClaims('/profile', [
+ has('button', { testID: 'save' }),
+ press('save').dispatches(token('save-profile')), // token() = claim
+ // matcher; action() = the authoring wrapper that declares the token
+ press('save').nativePointReachable(),
+]);
+
+// (string arguments resolve as claim-local handles — testID within the
+// claim's scope; selector forms like has('button', {...}) are sugar)
+// second-phase form: component-registered (mechanism below)
+claims(ProfileScreen, [
+ has('button', { testID: 'save' }), // claim-local: resolves within this
+ press('save').nativePointReachable(), // instance's subtree, not root scope
+]);
+```
+
+- **Claim kinds declare minimum evidence facets over a proof graph, not
+ a ladder.** Interaction evidence facets — `semantic-wired`,
+ `native-hit-reachable`, `responder-delivered`,
+ `expected-handler-observed`, `resulting-commit-attributed` — form a
+ directed graph in which `semantic-wired` and `native-hit-reachable`
+ are *independent* nodes (either can hold without the other). Each claim
+ kind names the graph nodes, edges, and join grades it requires; any
+ ordered "tier" label derived from the graph is a display aid with no
+ authority (the LLP 0374 W3 rule, which Exact's causal-trace code
+ restates). No overloaded "operability" boolean can overstate weaker
+ evidence:
+ - `has` / `missing` / `count` / `state` (enabled / disabled /
+ checked / unchecked — live in Exact today; presence itself is expressed there
+ via `has`/`missing` — though Phase A's implementation performs geometry-visible selection
+ for *all* selectors (every emitted selector carries
+ `visible: true`), while the `interactable: true` expectation comes
+ only from the `visible` state-clause lowering, so only the runtime monitor approximates raw committed-tree
+ presence, and the pair is worse than incoherent for `hidden`:
+ runtime `state ... hidden` is currently *unsatisfiable* (zero
+ matches fail before state evaluation; any match reports `hidden`
+ violated) while Phase A lowers `hidden` to absence from the
+ visible-filtered projection — a fourth Exact defect this analysis
+ surfaced — and **`present` is RASP's term**, motivated by
+ Exact's conflicting `visible` implementations below) —
+ semantic-structure facts, each carrying a **semantic-scope coverage
+ facet** (`complete` | `partial` | `opaque` | `unavailable`) for the
+ scope it queried, because a partial tree cannot prove absence: a
+ positive `has` witness may pass under any coverage, but `missing`
+ passes only under `complete` coverage (no-match under incomplete
+ coverage ⇒ `unresolvable`), and exact `count` requires `complete`
+ coverage — otherwise it reports only a lower bound. (Exact's Phase A
+ is the unsafe precedent here: zero observed matches passes `missing`
+ with no completeness fact — a node hidden in an opaque provider
+ subtree would false-pass. Fixtures: opaque-provider, failed-provider,
+ stale-coverage, contradictory-coverage, and the opaque-duplicate
+ case — a visible match plus an unseen duplicate in an opaque region
+ makes `state`/`press`/`change` targeting ambiguous, so those claims
+ require complete scope or independently sealed target uniqueness.) `present` (one term, no alias; `has(selector)` is its authored
+ syntax — `present` names the *predicate* a `has` witness proves) is
+ deliberately the strongest semantic-only visibility-adjacent claim: **`visible` is *not* a
+ semantic fact.** A clipped, off-viewport, transparent, or occluded
+ node is semantically present while imperceptible (Exact's own
+ precedent is incoherent here — its Phase A lowers `visible` to
+ `interactable` while its runtime witness treats committed-tree
+ presence as visible — which is the cautionary tale, not the
+ import). In RASP, `visible` is a proof-graph claim over committed
+ layout, viewport/clipping, mounted-native geometry,
+ opacity/transform, and the declared occlusion scope, with residuals
+ where system surfaces cannot be enumerated — and negative fixtures
+ for offscreen, clipped, transparent, native-transformed,
+ same-app-occluded, and system-occluded content;
+ - `press(x).dispatches(a)` — `semantic-wired`: the handler bound to
+ token `a` ran when semantically invoked; proves nothing about
+ reachability or real input routing;
+ - `press(x).nativePointReachable()` — `native-hit-reachable`: a witnessed
+ counterfactual native hit test at the target's interaction point
+ (the literal names `nativePointReachable`/`responderOperable` are
+ adopted here outright, per the naming-tracks-proof-strength rule —
+ the earlier `userReachable`/`userOperable` names sounded stronger
+ than what they prove and made overclaiming easier),
+ **evaluated at the scenario step that exercises the claim** (a
+ target requiring scroll-into-view or modal state is only meaningfully
+ probed in that state); `absent` availability (for any reason) ⇒
+ verdict `unresolvable`, never `passed`. This is *not* proof the real
+ responder path works — gesture arbitration, presenter guards, or
+ native handler bindings can still be broken (the semantic-mode gaps
+ Exact's input-fidelity authority reserves realistic input for). v1
+ scope: point interactions (`press`/`tap`) only — drag/pan/swipe need
+ trajectory reachability, which is not designed here;
+ - `press(x).responderOperable(token('save-profile'))` — the expected
+ token is named *in the claim syntax* (or bound normatively to one
+ unambiguous sibling `dispatches(token(...))` claim); inferring it
+ from the live binding is circular — a wrong handler would become
+ its own expectation — and a missing, changed, or ambiguous token
+ identity yields `unresolvable`. `expected-handler-observed` via real
+ input: one input from the in-process realistic tier (§3.2, where its
+ propagation proof exists) or an OS driver, causally joined at
+ `ingress-propagated` or `exact-key` grade through native ingress and
+ the responder path to the expected action token. This certifies
+ "operable through the app's responder path, **modulo occlusion
+ classes outside the reachability probe's declared scope**" (§3.3) —
+ **input plumbing, not product correctness**: a correct token can run
+ a no-op or wrong implementation and still pass;
+ - `press(x).causes(assertion)` — the v1 **causal postcondition**
+ claim: requires the expected action token, a same-action
+ `resulting-commit-attributed` join, and the assertion evaluated
+ against that attributed state — a pre-existing or
+ unrelated-concurrent-commit truth must not pass. Provider/domain
+ outcomes (the mutation actually landed) require authoritative
+ mutation receipts (the LLP 0278 rule) or return `unresolvable` —
+ UI-state postconditions are what v1 can certify. Fixtures:
+ expected-handler/no-change, wrong change, pre-existing
+ postcondition, unrelated concurrent commit, an ambient update
+ satisfying the assertion in the *same coalesced commit* as the
+ target action (fail closed when per-change provenance cannot
+ distinguish the causes), backend failure, optimistic rollback;
+ - `press(x).navigatesTo(route)` — a route-outcome claim with the same
+ causal contract every claim kind carries: it requires the expected
+ action-token observation causally joined, at a named verification
+ grade, to an Expo Router transition and settled-route fact (§3.0
+ router row) on the same attested run. Ambient or merely
+ contemporaneous navigation (redirects, timers) must not pass; absent
+ causal evidence ⇒ `unresolvable`. Navigation is Expo's most
+ differentiated runtime surface, and the Exact precedent here has
+ three distinct levels: *static navigation-declaration checking
+ ships* (dead links, params, redirects, alternates, exits —
+ `packages/exact-devtools/src/navigation-contracts.ts`); *live
+ settled-route receipts against declared edges ship* and attach to
+ affordance-driven interactions — carrying the *matched declared
+ edge's* metadata, not an observed interaction-provenance join
+ (`packages/exact-router/src/navigation-receipts.ts`); what does
+ *not* exist anywhere is the propagated action-token→transition
+ causal join — Phase A does not consume its emitted action identity —
+ so that join, and only that join, is new work;
+ - `press(x).osDelivered()` — the delivery leg alone (driver input fact
+ at verification grade); insufficient for operability unless it joins
+ to the same handler observation as `responderOperable` requires. Bare
+ `armed-bracket` and `heuristic` never satisfy it.
+- **Two claim scopes, sequenced to decouple identity risk.** Claims
+ attach at two scopes: **route/root-scoped** claims and
+ **component-registered** claims (the `claims(Component, ...)` form).
+ Route scoping avoids React *component* identity, but it still needs
+ **route-instance identity**: a pathname names the claim *declaration*,
+ while evaluation and ledger entries bind to the mounted instance —
+ app/root incarnation, router entry key, mounted-route incarnation, and
+ params/location identity — because retained stack entries, two entries
+ with the same path and different params, and the same route in
+ multiple roots/windows are all distinct surfaces. Conformance cases
+ cover exactly those three, plus Expo Router layout routes and groups
+ (a route rendered inside nested layouts/shared segments is a fourth
+ identity wrinkle: instance keying must state whether it sits below or
+ above the layout-route boundary). Route-scoped claims ship *first*: they
+ let the evidence protocol and verifier earn reliability before taking
+ on React registration semantics, and for Expo Agent's generated apps —
+ where routes are the unit of generation — they cover most of the
+ value. Component registration follows with the mechanism below, and
+ route-scoped claims remain first-class permanently — they are the
+ natural unit for generated apps, not a compatibility tier awaiting
+ removal.
+- **Registration vs attribution identity.** Registering claims against a
+ component (`claims(ProfileScreen, ...)`) is a *mount-time* association —
+ a different use of identity than cross-render dispatch attribution. v1
+ supports exactly one mechanism: an explicit registration handle wrapped
+ at export (or applied as a hook), which fails loudly on HMR remount and
+ rejects anonymous `memo`/HOC-wrapped usage rather than silently
+ re-associating. Mechanism sketch: the handle carries a module-scope
+ token minted at registration; a mount whose component identity does not
+ carry the token (HOC wrapping) is rejected, and a remount presenting a
+ new token under an existing registration name fails registration until
+ explicitly re-registered. Richer registration behavior is the v2 design
+ (§6).
+- **Per-instance scope, claim-local handles.** Claims register against a
+ mounted instance and check within its subtree; multiple instances check
+ independently. Handles inside a claim resolve claim-locally
+ (`{claimInstanceId, localTestID}`), so reusable library components can
+ ship claims without colliding with root-scoped app handles (§3.5).
+ Exact precedent here is *partial*, in two ways: its Phase A selector
+ runner searches the supplied root rather than an instance subtree; and
+ while its monitor evaluates witnesses per mounted instance internally,
+ the public agent join queries by component *name* and attaches all
+ same-name witness results to each handle — instance identity is lost at
+ exactly the boundary RASP cares about. Fully instance-scoped
+ verification is therefore a RASP requirement, not a proven import, and
+ "two same-name sibling instances with divergent state" is a required
+ conformance case.
+- **Action tokens, not function identity:** `action('save-profile')` is an
+ opaque stable token attached at the event binding and carried on
+ dispatch receipts. Authoring side, the v1 sketch is a wrapper —
+ `onPress={action('save-profile', handleSave)}` — with a hook variant for
+ dynamic handlers, and Expo Agent's codegen emits tokens by default.
+ React function identity across re-renders and wrappers is unsuitable
+ and is not used. Where no token is declared, `press(...)` claims weaken
+ explicitly to "dispatched something" — and the *weakening rate* is a
+ tracked metric: at M1 as a static proxy (share of interactive nodes
+ with declared tokens), and as the true tokenless-claim rate once
+ claims run at M3; it measures whether the authoring surface is
+ actually adopted — and M0's frozen metric set either includes a
+ token-coverage floor for the M3 gate or states explicitly that v1
+ token coverage is an Expo Agent codegen property with brownfield
+ adoption a non-goal, so the metric has a decision attached, not just
+ a dashboard.
+ (Exact provides partial precedent here: Acto tap receipts can carry
+ action/owner provenance — an optional field populated from eligible
+ Contract dispatches under the dev-mode interaction context, not from
+ arbitrary interactions — though its Phase A verifier does not yet
+ consume it. Token *registration* for arbitrary React is the new part.)
+- **No state guards in v1.** `when(s => ...)` requires a defined fact
+ projection from component state, per-instance evaluation scope, and a
+ redaction story; state-guarded claims are a v2 design item (§6).
+- **Verdict vocabulary:** every claim evaluation returns exactly one of
+ `passed` | `failed` | `unvisited` | `unsupported` | `unresolvable` |
+ `timed-out`, plus the evidence facets it consumed. Verdicts are
+ themselves attestation-bound artifacts (§3.0): each names the attested
+ tuple it was evaluated against. An **observation ledger** records which
+ required claims were exercised by which scenario, and a run renders to
+ a human-auditable, diff-stable **evidence report** — a verification
+ manifest on two axes, proof strength × obligation coverage
+ (route instance × action token × consequence class × required outcome
+ × strongest profile), so the *negative space* a bare profile name
+ hides is visible — the artifact Expo Agent
+ attaches to a PR or build, the honesty machinery made visible to end
+ users, not only to gates. The normative schema and wire corpus are
+ named independently of the interim "RASP" working name, so the M0
+ rename cannot perturb conformance work.
+- **Certification profiles, and "done" gating without a weak-gate
+ escape.** Three named profiles expand mechanically to required graph
+ nodes, join grades, and **declared occlusion residuals**, so marketing,
+ agents, and CI cannot blur tiers. Profiles and milestone exits are
+ **per platform** — a profile certified on iOS says nothing about
+ Android:
+ - `semantic-development` — required claims `passed` with
+ ledger-verified exercise, plus `nativePointReachable` by default for every
+ user-gesture claim (exceptions: explicitly declared per claim,
+ audited, and never able to satisfy an operability assertion).
+ Residual: everything past semantic wiring + probe-scoped
+ reachability;
+ - `responder-operability` — `semantic-development` plus `responderOperable`
+ for user-gesture claims. Residual: occlusion classes outside the
+ probe's declared enumeration scope (e.g., cross-process system
+ surfaces on iOS) — a system-overlay conformance fixture must show
+ this profile does **not** report green under a system surface it
+ cannot see; it reports the declared residual. Wherever such a
+ residual exists, this profile's result is **always visibly
+ qualified** ("responder path verified; system occlusion unverified")
+ and can never be rendered or exported — by Expo Agent, CI, or any
+ durable artifact — as an unqualified pass;
+ - `release-configuration-attested` — `responder-operability` on an
+ attested **instrumented release-configuration sibling**: a build
+ from the same source revision and effective configuration as the
+ release candidate, in release configuration, differing *only* by the
+ verification channel — with the shared and differing inputs
+ themselves attested at `measured`/`signed` class (§3.0). The
+ artifact model is chosen deliberately and its limit stated in the
+ profile's own name: because production builds strip the channel
+ (§3.5), the exact distributed bytes carry no verifier, so v1 makes
+ **no claims about shipped store bytes, period** — this profile
+ certifies the release *configuration*, and its language must say so.
+ (Exact-byte attestation — a dormant authenticated channel in the
+ shipped binary, or two-artifact attestation of shared payload
+ sections plus wrapper — is a distinct threat/size/privacy design
+ deferred to §6; if ever built, a successor profile can claim bytes.)
+ A differential run between instrumented and uninstrumented siblings
+ cannot prove byte equivalence, but it bounds the verification
+ channel's behavioral and performance perturbation, and is part of
+ this profile's evidence.
+ The profile's driver leg **requires verification grade**
+ (`ingress-propagated` or `exact-key`, per §3.2); if OQ4 resolves
+ pessimistically on a platform, this profile is *unattainable there*
+ and release-grade claims remain unavailable — recorded as such,
+ never quietly weakened. This is the strongest profile, per platform,
+ after an end-to-end run — and even it never authorizes
+ "shipped-app-verified" language, only
+ "release-configuration-verified."
+
+ **The consequence classifier is named, and its default fails safe.**
+ Consequence class is a provenance-bearing descriptor on the action
+ token (declared in code, emitted by Expo Agent codegen, lint-checked);
+ task-critical navigation obligations ride the same descriptor on
+ `navigatesTo` claims. **Unclassified exercised interactive gestures
+ default to consequence-bearing** — the obligation applies unless an
+ explicit, audited downgrade exists, so a generator that forgets to
+ label a destructive action gets *more* obligation, not a silent
+ fallback to `semantic-development`. Classification coverage is a
+ tracked, gated metric, with a mutation fixture: an unlabeled
+ destructive action must still acquire the `responderOperable` obligation or
+ fail the completion decision loudly. The escape valve gets teeth too:
+ **downgrade share is tracked with an M0-set ceiling**, and a downgrade
+ audit means a named approver and a recorded artifact — otherwise the
+ rational response to expensive obligations is mass downgrades and the
+ fail-safe default is theater. (Gestures inside third-party library
+ components the author cannot token-annotate will be a common
+ obligation source; library claims — §6 — and claim-local handles are
+ the intended answer, and the downgrade ledger will show whether they
+ suffice.) The profile↔mechanism contract is a normative table, not
+ prose:
+
+ | Profile | Input mechanism | Same-action join required | If unavailable |
+ | --- | --- | --- | --- |
+ | `semantic-development` | semantic invocation + native point-reachability probe for gesture claims | n/a (probe is counterfactual) | probe `absent` ⇒ gesture claims `unresolvable` |
+ | `responder-operability` | in-process tier **or** OS driver — either | `ingress-propagated`/`exact-key` to the action token | unattainable on that platform; recorded (§3.2 disable rules) |
+ | `release-configuration-attested` | `responder-operability` **plus `osDelivered`** (OS driver required) | verification grade, joined to the same handler observation | unattainable; recorded, never weakened |
+
+ Profiles expand to declarative proof obligations and are
+ property-tested: generated counterexamples for monotonicity,
+ cross-action substitution, missing evidence, and attempts to compose
+ two individually insufficient facts into a stronger verdict. Expo
+ Agent's generalized product "done" is gated twice. First, on a
+ **closed obligation universe with a sound closure authority** — and
+ the honesty here is that arbitrary React Native has none: Exact's own
+ navigation analysis records that React edges cannot be statically
+ closed, and LLP 0085's `exits only` is an *authored* exhaustive
+ declaration, not inferred completeness. So generalized "done" is
+ scoped to the one place closure is real: **Expo Agent–generated Expo
+ Router apps under a build-sealed obligation manifest** —
+ proof-carrying generation output (Metro/Router/codegen emits the
+ manifest of routes, event bindings, action tokens, consequence
+ classes, and required outcomes; native and provider semantics
+ contribute bounded submanifests; runtime registration reconciles
+ against it; the attested artifact binds its content-addressed digest;
+ dynamic escape hatches disqualify). Closure feasibility is an M0
+ experiment and an M3 entry gate. Brownfield and hand-authored apps
+ keep the honest weaker result permanently. And one limit is stated,
+ not papered over: **the generated manifest closes implementation, not
+ intent** — the generation authority cannot emit an obligation for a
+ feature absent from its own inputs, so a "delete account" that
+ vanished from both code and manifest is invisible to it. The verdict
+ tiers accordingly: the manifest-scoped result is
+ **"build-sealed implementation obligations verified on
+ ⟨platform/profile⟩"** — unqualified "done"/"product complete" is
+ prohibited at every tier — and the stronger
+ **"sealed specification satisfied"** exists only where an
+ *independently authored, frozen pre-build acceptance manifest* is
+ attested alongside the artifact-derived one and the two reconcile
+ bidirectionally (Expo Agent must never silently rewrite the standard
+ its own output is judged by). The omission mutation is stated
+ satisfiably: an *implemented* destructive action omitted from the
+ obligation manifest must block; an intent-level omission is caught
+ only at the specification tier. Within manifest scope, every
+ obligation is exercised-and-passed or explicitly waived under audited
+ policy — and waivers cannot become the escape hatch the downgrade
+ ceiling already closes elsewhere: **a waived consequential obligation
+ blocks sealed/generalized verdicts or schema-forces
+ `qualified`/"declared scenarios verified" with a visible residual**,
+ waiver share is tracked with an M0-set ceiling, each waiver records
+ approver/rationale/artifact, and a mass-waiver conformance mutation
+ must fail; unknown, opaque, or unvisited *consequential* items block
+ the verdict, so an implemented-but-forgotten destructive button or
+ never-visited route cannot sit outside the ledger. And sealing closes
+ *declarations*, not the behavioral space they quantify over: the same
+ token behaves differently for `/item/1` vs `/item/2`, authenticated vs
+ not, permission branches, feature flags, or the hundredth list row —
+ exercising a declaration once verifies one point, not the domain. So
+ each obligation carries a **domain model** in the coverage-algebra
+ sense (declaration identity x route-parameter/data partitions x
+ precondition/state fingerprints x instance quantifiers x scenario
+ inputs — coverpoints with finite partitions, cross-coverage, waived
+ bins, unreachable bins, and residuals): the sealed verdict is
+ available only over *sealed, finite, covered partitions* (or an
+ accepted equivalence argument across an unenumerated dimension), and
+ any unbounded, opaque, or uncovered dimension restricts the result to
+ "declared scenarios verified" with an explicit residual. Mutations:
+ the same token passing for one parameter/item/state while failing for
+ another must not produce a sealed pass; a consequential affordance
+ created after ledger capture must block.
+ Mutations: an *implemented* destructive action omitted from the
+ obligation manifest, a newly added unvisited route, an untokenized
+ third-party gesture, and an action added after ledger capture — each
+ must block. Absent the closed
+ universe, the result is named **"declared scenarios verified"** and
+ generalized product-completion language is prohibited. Second, on
+ proof strength: **`responder-operability` for consequence-bearing
+ gestures** on each platform it claims — and for those same gestures a
+ `causes(...)` postcondition, since operability without outcome is
+ plumbing;
+ `semantic-development` may gate only explicitly-labeled development
+ checkpoints and is always reported under that name — never as
+ generalized "done." The profile-level result type is itself closed and includes
+ **`qualified`** — "passed with declared residual, never renderable as
+ green" is a normative representation, not just a presentation rule.
+ Profile names, residual qualifications, and
+ prohibited completion phrases are **schema-generated and linted** in
+ every consuming surface (Expo Agent UI copy, CI dashboards, evidence
+ reports): prose policy alone will not stop a dashboard from
+ abbreviating a qualified result to "passed." A flow whose presses passed only as `dispatches`
+ satisfies no profile. This is the §5 promise — "verification never
+ accepts semantic-only success" — made normative, without letting
+ `nativePointReachable` quietly play the operability role either.
+
+**Exact precedent, stated against current code (2026-07-21), not early
+docs** *(corpus-internal calibration detail — like the Revision History
+appendix, this paragraph — and every Exact-defect parenthetical in the
+claim-kind bullets above — is dropped from externally shared
+renderings)*: Contract's machinery today spans four mechanisms — compile-time
+checking (exact-set verification of `writes`/`derive` declarations over
+analyzable IR, stated precisely: the action-header `writes` list is
+*optional* — an action with no header compiles without write diagnostics;
+when a header is present, it is set-compared, with under-declaration an
+error and over-declaration a warning ("exact-set enforced" belongs to
+the authored contract-block clauses below, which error both ways); the
+contract-*block* clause forms (`action … writes`, `derive … depends`) are
+likewise optional and exact-set checked as errors when authored; and
+opaque import boundaries degrade to warnings in action/task contexts
+while strict project mode errors on them in pure/resource/query
+contexts — and an action with *neither* a header write declaration
+*nor* an authored contract-block write clause has **no** static or
+monitor write-set obligation at all: the runtime monitor checks only
+authored contract-block clauses, and an unvisited undeclared action
+produces no observed write-set evidence. One caveat cuts the other way:
+Exact's dev-time dataflow inspector collapses an absent header to an
+empty declared set and exact-compares it against observed writes,
+folding the disagreement into the agent-facing verdict (the raw
+`writeContractOk` exists for dev-registered Contract instances
+generally, while the verdict gate becomes contract-block-scoped through
+the verify-claims operation, which skips components without contract
+blocks) — a de facto observational gate that appears accidental; RASP
+obligations affect verdicts only when *authored*, by rule), runtime
+monitoring (`action-writes` checking on invocations — per-invocation
+`actualWrites ⊆ declaredWrites` against authored clauses, versus the
+static clause check's exact-set equality; explicitly the
+defense-in-depth layer for escapes the static picture misses — plus
+passive invariants, auto-watched for root-mounted components today),
+read-only witnessing (instance-scoped internally;
+see the public-join caveat above), and Phase A live verification —
+read-only assertions (`has`/`missing`/`state`, expression-valued counts)
+plus effectful interactions (`press`/`change` with outcome assertions),
+with guarded `when` live over a whitelisted subset of live
+assertion/interaction clauses (guards around excluded forms do not
+emit) —
+with `derive … depends` static-only, and the interaction evidence
+split at *three* distinct levels — direct semantic `press` witnesses
+that *some* press-family handler ran (not its identity, not
+reachability — the underlying tap receipt may carry optional Contract
+action/owner provenance, but Phase A does not consume it); direct
+`change` witnesses **value injection/commit only** unless separately joined to handler evidence (Exact's type
+operation commits the batch and forcibly reports `dispatched: true`
+even when the dispatcher found no Change binding — a third Exact
+defect this analysis surfaced); and the DOM fallback witnesses browser
+event *injection* only — no handler observation. RASP inherits none of this
+overloading: receipts carry distinct `valueCommitted`,
+`handlerObserved`, and `nativeIngressObserved` fields, never one
+`dispatched` boolean; the react-native-web arm (§4 M-1) must expose
+which level it witnessed and can never promote an injection-only path
+to `semantic-wired`; and a no-Change-handler fixture proves the
+distinction
+(`packages/exact-contract/src/compiler/analyze.ts`,
+`packages/exact-contract/src/runtime/contract-monitor.ts`,
+`packages/exact-contract/src/runtime/dataflow-inspector.ts`,
+`packages/exact-devtools/src/agent/contracts.ts`,
+`packages/exact-devtools/src/agent/contract-inspection.ts`,
+`packages/exact-devtools/src/agent/input.ts`,
+`packages/exact-router/src/navigation-receipts.ts`). The action-token and
+minimum-evidence-facet designs above are RASP's additions, not ports.
+
+The compile-time checking Contract gets from its closed world —
+exact-set *error* enforcement for authored contract-block
+write/dependency clauses, asymmetric diagnostics for optional
+action-header write declarations, and syntax/name/purity validation of
+guarded clauses — is out of scope for arbitrary React and stays an
+Exact-side differentiator. (Not because "JSX is not statically analyzable" — lint
+rules, schema checks, and literal-handle validation are all possible and
+worth shipping — but because sound whole-component effect and dependency
+inference is not available for arbitrary JS.)
+
+### 3.5 Delivery and positioning
+
+- **Logical schema first; transports are projections.** RASP is a
+ versioned schema with a **renderer-neutral evidence core** and
+ separately versioned Expo Router / EAS / Expo Agent extensions (a
+ single Expo-shaped schema would raise adoption costs for exactly the
+ consumers the standardization thesis needs) — a schema (tree, identity tiers, evidence envelope, receipts,
+ diagnostics, claims verdicts); HTTP/WebSocket on the dev server and MCP
+ are projections of it. (Acto precedent, stated accurately: its
+ operations ship over HTTP/MCP today — the lesson exported here is
+ schema/authority discipline, not a wire format.)
+- **Discoverable by construction (the LLP 0365 lesson, mechanically):**
+ the capability manifest — supported operations, evidence facets, and
+ degradation states — is *generated from the same operation/evidence
+ registries the implementation runs on* and validated against the
+ conformance corpus, so the manifest cannot drift from the registries —
+ though behavioral drift beyond what the conformance corpus covers
+ remains possible, which is why the corpus grows with the schema;
+ clients negotiate rather than hard-code an assumed level. RASP also ships a first-party agent
+ skill/onboarding surface (the analog of Acto's onboarding op and the
+ generated `exact-inspect` skill) — with LLP 0365's *actual* design,
+ not a generated approximation of it: an **authored intent/capability
+ and GAP authority** (aliases, owner, expiry, remedy, runnable
+ examples), joined *bidirectionally* to the generated
+ operation/evidence registries with implementation witnesses where
+ shipped — because an implementation-generated index cannot name a
+ capability that does not exist, and GAP honesty is the point. Clients negotiate
+ against the schema version too: a consumer pinned to schema vN meeting
+ a vN+1 runtime gets a declared compatibility answer, not undefined
+ behavior. Capability that exists but agents
+ can't find is capability that doesn't exist.
+- **Partners as co-designers and contributors.** SwM and Callstack review
+ at M0 (review is sought and scheduled, but Expo-controlled engineering
+ exits never depend on it — §4); the driver-correlation protocol (§3.2)
+ gives them a role no adapter can replace; a published conformance suite
+ plus a recorded wire corpus (including degraded and adversarial cases)
+ makes "consumes RASP" a test result rather than a bilateral claim —
+ and the suite includes a **result-presentation case**: a qualified
+ `responder-operability` pass rendered unqualified fails conformance,
+ because Expo's own linting cannot bind partner surfaces.
+- **Development security is in scope from the start.** Not just a bearer
+ token on mutations: RASP reads expose structure, geometry, labels,
+ screenshots, and (via the provenance extension) component identity —
+ sensitive on their own. Pre-M1 threat model covering: target
+ registration and principals (including the Expo Go case, where one
+ long-lived host app loads many projects and target identity ≠ app
+ identity); loopback-by-default with explicit opt-in for LAN/tunnel/CI
+ transports — every non-loopback hop requires authenticated encryption
+ (or an explicitly trusted encrypted tunnel) with channel binding and
+ replay protection, since credentials, screenshots, and contributed
+ evidence traverse it, and destructive-effect confirmations are
+ host-owned and bound to the exact operation, target, principal, and
+ consequence descriptor; Host/origin validation and anti-DNS-rebinding;
+ token scope and rotation; token-gated sensitive reads, not only mutations; redaction
+ defaults covering visible text, input values, screenshots, claim
+ scenarios, and retained receipt history — not only props (no arbitrary
+ prop or function serialization, allowlists only); contributor
+ authentication and evidence ceilings (§3.2); audit receipts; and
+ guaranteed build-stripping/default-off in production builds. The
+ genuinely borrowed controls are Exact's converged posture — loopback
+ defaults, target registration, Host/origin validation, and
+ authenticated effects and selected sensitive host reads (ENG-23118;
+ posture documented in docs/agent-api.md and the original
+ agent-feedback-system RFC). Token-gating *ordinary* structure/screenshot
+ reads goes beyond what Exact ships today (its loopback reads are
+ open); RASP deliberately strengthens that posture rather than
+ inheriting it.
+- **Handle discipline as convention, specified:** app-level `testID`s are
+ unique per screen/root and lint-enforced; list items use stable per-item
+ handles (not index-derived); reusable components use claim-local handles
+ (§3.4) rather than competing for root-scoped names; production stripping
+ policy is explicit; docs, lint rule, and Expo Agent's codegen treat
+ handles on significant nodes as required. The handle quality determines
+ everything above it — and handle *scoping* determines whether identity
+ survives the public API: Exact's own witness join loses instance
+ identity precisely because its public query key is a component name,
+ even though internal evaluation is instance-scoped (§3.4). Name-keyed
+ public APIs are how instance identity dies; that is the argument for
+ claim-local handles.
+- **Dev-mode first, dev clients first.** Ships in dev clients, off in
+ production builds by default. **Scope: New Architecture only** — the
+ fact table is built on Fabric; old-architecture apps are out of scope,
+ full stop. Expo Go delivery comes only after a dedicated
+ compatibility/isolation gate (§4 M-Go): Go's one-host-many-projects
+ model makes target identity ≠ app identity, and shipping the layer in
+ Go charges binary-size/startup cost to every Go user regardless of
+ agent use, so Go inclusion carries its own payload budget and
+ cross-project isolation conformance tests. A production tier (opt-in,
+ authenticated, redacted) is deliberately deferred (§6); a *tethered
+ release-configuration verification artifact* (the instrumented
+ release-configuration sibling of §3.4 + its dev-only verification
+ channel, pre-submission) is defined at M3 so "works in the dev
+ client" is never silently promoted to release-grade language — and
+ release-grade language itself stays scoped to the release
+ configuration, never distributed store bytes (§3.4).
+
+### 3.6 Explicit non-goals
+
+- **Not a driver/automation CLI.** Device orchestration, OS-level and
+ device-level input synthesis, video capture, CI replay belong to
+ agent-device, Argent, and Maestro. (The in-process responder-path tier
+ of §3.2 is runtime-authority and in scope — it synthesizes events only
+ inside the app's own process and windows, which no external driver can
+ do with minted ingress identity.)
+ RASP defines how their input-delivery facts join the evidence model
+ (§3.2); it does not replace them — OS-delivered input is a fact the
+ runtime cannot produce, only consume (§3.0's fact/authority table; the
+ false-positive consequence of forgetting this is §2.2 item 5).
+- **Not Contract-for-Expo.** Porting Exact's authoring notation is a
+ separate, larger bet (an incubator experiment at most, timed after
+ Contract's notation freeze); this RFC takes only the verification model,
+ which carries no notation risk.
+- **Not a devtools UI.** React DevTools remains the human surface; RASP is
+ the machine surface. They may share plumbing.
+
+### 3.7 Capability provenance matrix
+
+What this proposal actually rests on, per capability:
+
+| Capability | Provenance |
+| --- | --- |
+| Snapshot economy, snapshot-local refs, loud staleness on changed targets, annotated screenshots | **Shipped Acto precedent** (daily agent use — author-reported); receipted `clean` resolution and the semantic-id ladder placement are **deliberate RASP adaptations** (§3.1) |
+| Receipted `stale-verified` + graded staleness, subtree precision | **Adaptation** — implemented in Exact but flag-gated (LLP 0321 C1 riding default-off B1), with an event-kind-only equivalence check; RASP's target-equivalence fingerprint (§3.1) is **new design** |
+| Target/artifact attestation binding | **Adaptation of LLP 0374 W1** (expectation-binding sessions); the RN/Expo attestation protocol is **new design** |
+| In-process realistic input tier | **Adaptation** — Acto's shipped LLP 0306 P1 tier (windowSendEvent / touch-sequence); the RN responder-path implementation with minted ingress identity is **new work**, feasibility spiked at M0 |
+| Cross-snapshot identity tiers | **Adaptation** — Acto's two-tier model; recycling/virtualization open in both contexts |
+| Receipts with evidence envelope | **Adaptation** — Acto `observeAfter` + `nativeReachability` + LLP 0374's facet model; the normative envelope and commit-causal attribution are **new design** with 0374's propagated-id requirement |
+| Diff-first observation economics | **Open evidence** — Exact's own results mixed (LLP 0326); report-only here, never a gate |
+| Native-reachability facts, escalation ladder | **Shipped Acto precedent** (shipped before and validated by the Partitime boundary) |
+| `resolve_target`, `stacking`, diagnostics doctrine | **Shipped Acto precedent**; a named `why_invisible` with causal constraint provenance, and all Fabric/Yoga/native-host implementations, are **new RN work** |
+| Claims tier | **Adaptation** — LLP 0085's model with substantial live machinery in current Exact code (§3.4 matrix); action tokens, minimum evidence facets, verdict vocabulary, ledger, claim-local handles are **new design** |
+| Driver correlation + contributed-evidence trust model | **New design** (LLP 0374's evidence classes as input) |
+| Fabric/Fiber/native joins, snapshot coherence, concurrent-React attribution | **New RN-specific design** (M0/M1 spike territory) |
+| Landscape table, partner posture, Expo Agent internals | **External assumption** — sourced §2.1, validated at M-1/M0 |
+
+## 4. Milestones
+
+Two bounding disciplines govern the whole plan. **Every M-1 item and M0
+spike is a bounded experiment**: each carries a staff-week and calendar
+ceiling (numbers assigned in the M-1 funding brief, structure fixed
+here), spikes are ordered by information value, each has a preregistered
+stop/narrow/pivot outcome, and M0 is a set of independently gated
+experiments, not one cumulative exit — the M0 brief tables each spike
+(ceiling, dependency, stop/narrow/pivot result, which milestone it
+unlocks, and whether failure removes an optional layer or blocks the
+minimum core). And the protocol has a named
+**minimum viable core** — host semantics, identity tiers, opaque-region
+reporting, layout + native-reachability facts, attestation, and
+conformance — with Fiber provenance, claims, content-provenance
+tainting, and release certification as separately fundable layers; the
+MVP core is what still justifies M1 if the provenance or attribution
+spikes resolve pessimistically. After M0, the work splits into
+separately governed specifications (core schema/conformance;
+renderer/native joins; input identity and driver integration; claims
+and scenario execution; release attestation) — this RFC remains the
+strategic map, never the single implementation contract.
+
+The thesis split is closed over the whole decision contract by a
+normative **thesis-by-milestone matrix** (maintained as a
+content-addressed decision ledger — changes require a new approved
+version, not prose drift):
+
+| Gate / deliverable | Reliability thesis | Standardization thesis |
+| --- | --- | --- |
+| M-1A ecological prevalence gate | **required** (funds build arms) | inapplicable |
+| react-native-web prototype | required *within* the funded reliability build arms (conditional on M-1A) | inapplicable |
+| Schema/ledger + partner-appetite arm | optional | **required** (own demand signal) |
+| M0 core spikes (host-semantic join, identity/coherence/attestation, reachability) | **required** | **required** (protocol integrity is universal) |
+| Cheap-arm superiority + held-out false-pass improvement | **required** (economic gates) | inapplicable — replaced by **executable partner consumption, gated pre-M1**: at least one non-Expo design partner runs the schema/ledger and conformance corpus over its own recorded facts (via a provided recorded-fact adapter) and names an integration owner and timeline or ships a minimal adapter; absent that, any native M1/M2 spend under this thesis is explicitly labeled Expo-funded option value, never evidence-backed standardization |
+| M2 shadow calibration + adjudication | **required** | **required** |
+| M3 claims product + agent-in-the-loop gate | **required** | inapplicable |
+| M4 external adoption evidence | optional upside | **required** |
+
+A failed reliability gate kills only the reliability branch — it never
+blocks a standardization program that has its own demand signal — and
+vice versa. Universal protocol-integrity requirements (zero-false-pass
+negatives, envelope conformance, fail-closed attestation, the
+result-presentation rule) bind both branches; cheap-arm superiority and
+false-pass improvement are reliability-only economics. And where the
+M0 vertical slice meets an unavailable optional layer (commit
+attribution), the slice **passes by representing the absence
+faithfully** — an explicitly absent/heuristic edge whose dependent
+claim verdict is `unresolvable` — rather than blocking the core.
+
+Metric definitions and thresholds are frozen at M0 and versioned with the
+fixture corpus; deterministic negative fixtures permit **zero false
+passes**; every fixture carries an **expected-fact manifest** (node
+recall, event descriptors, geometry, identity/staleness transitions,
+opaque regions, evidence availability) so positive semantic completeness
+is tested, not assumed — a tree that omits bindings or nodes cannot pass
+by succeeding at a task. Observation-economics numbers (screenshot count,
+tokens) are **report-only throughout** — reliability metrics gate,
+economics inform.
+
+- **M-1 — exploratory baseline (labeled as such).** A short-form pitch
+ (problem, evidence, the M-1 ask with its ceilings, kill criteria —
+ and the non-negotiable minimum on its first page, so no trimmed
+ variant is ever presented without it)
+ precedes the funding ask — the M0 decision brief has a smaller M-1
+ sibling, names actual people/teams for the three independence roles
+ M-1 depends on — sponsor/DRI, held-out corpus custodian, non-author
+ incident adjudicator; the custodian and adjudicator sit **outside the
+ sponsor's reporting chain and independent of every evaluated tool
+ vendor** (an external reviewer preferred; a partner engineer only for arms in
+ which their own tool is not scored), because with a CEO sponsor
+ "non-author" alone does not buy independence (permanent ownership is an M0 exit) — and the M0
+ decision brief presents the trimmed-to-the-non-negotiable-minimum
+ variant as a first-class costed option, not a fallback footnote. Preregister the
+ incident taxonomy, sampling policy, and tool/version pins, then run
+ current Expo Agent and current outside-in tooling against the shared
+ positive/negative fixture corpus (a11y-hidden nodes, duplicate labels,
+ stale snapshots, virtualized lists, portals/modals, native overlays,
+ non-Fabric regions, ambient noise), and catalog real false-success
+ incidents from the field — and, separately from the adversarial
+ corpus, a **blinded, stratified, denominator-bearing ecological
+ sample** of ordinary Expo Agent generation/repair tasks, reporting
+ failure-family *prevalence* (base rates, not just detector
+ capability), abstentions, the independently adjudicated
+ RASP-addressable share, and cheap-arm recovery. M-1 is split
+ accordingly, with **thesis-specific gates preregistered in the
+ brief** so neither thesis can suppress or revive the other post hoc:
+ **M-1A** (the ecological baseline + cheap-arm comparison) runs first;
+ the *reliability-thesis* build arms (the react-native-web prototype)
+ are funded only if M-1A clears a preregistered
+ addressable-prevalence threshold — "measure before building,"
+ literally — while the *standardization-thesis* arm (the schema/ledger
+ experiment and partner-appetite test) proceeds on its own separate
+ demand signal: the entrance-diligence confirmation that no supported
+ renderer contract exists, concrete partner integration interest, and
+ an acceptable compatibility-cost ceiling. Low Expo Agent failure
+ prevalence kills the reliability build arms without silencing the
+ standardization evidence, and vice versa. Include **cheaper-alternative arms** on the
+ same corpus: (a) stricter handle discipline + best-current outside-in
+ tooling, and (b) a devtools-only JS instrumentation variant — RASP must
+ beat the best cheap arm, not just the status quo. Also stand up a
+ **schema validator/ledger over existing outside-in facts** — a low-cost
+ test of the evidence vocabulary, the contribution model, and partner
+ standardization appetite before any Fabric work. Each field incident
+ is packaged as a privacy-preserving **fact-gap card** (minimized
+ replay, ground truth, decisive missing fact, current-tool ceiling,
+ proposed producer with a feasibility field — supported hook /
+ plausible upstream hook / maintained patch / unknown — so
+ "addressable" is never mistaken for "economically feasible" before M0
+ prices the seam, disclosure policy, raw-data custody) so partners
+ and adjudicators can judge incidents without routinely receiving full
+ source traces. A named deliverable of the reliability build arms (conditional on the
+ M-1A gate), with its own exit, is the **react-native-web arm**: deploy the envelope and claims runner on
+ DOM joins — "easy" is a timeboxed feasibility hypothesis, not a
+ budget assumption (LLP 0136 did not pin react-native-web internals) —
+ with identical Expo Router state, earning authoring
+ feedback on the claims vocabulary and profiles while the Fabric spike
+ carries the schedule risk — the protocol half de-risked independently
+ of the hard native half. It is a **disposable, non-normative
+ vocabulary prototype**, operating under the M3 effect-safety boundary
+ from its first effectful run (fixture accounts, denied external
+ effects, reset semantics — safety is not deferred to M3): its results satisfy no native feasibility,
+ operability, or certification gate, its verdicts declare which
+ evidence level they witnessed (a DOM event injection is not a handler
+ observation), and it doubles as the pilot for token-emitting codegen
+ so the wrapper ergonomics are pressure-tested before M3 — with a
+ preregistered output contract (the claim-syntax changes its
+ vocabulary feedback can force before M3) and a mechanical sunset:
+ the arm's code is not carried past the M0 schema freeze, so
+ disposability is enforced rather than promised.
+ Incidents are
+ classified as *RASP-addressable* under a **mechanical, preregistered
+ rule** — an incident qualifies iff the missing or wrong fact belongs to
+ the §3.0 fact table at an evidence grade the incident's tooling
+ demonstrably could not produce, reported as two separate figures — a *protocol-relevant fact gap*
+ (some §3.0 fact was missing) and the stricter *RASP-addressable*
+ (additionally: a plausible named producer exists and the missing fact
+ was decision-relevant), so absence alone is never treated as economic
+ solvability — with the M-1 brief binding the
+ classifier to a **content-addressed fact-table/taxonomy version**:
+ retroactive reclassification of the funding gate is forbidden, and any
+ M0 definition change is reported as a new classifier version, never a
+ rescore — and the classification is adjudicated
+ by a party that is not the author (a partner reviewer or independent
+ engineer, consistent with M2's named-oracle discipline), because
+ "RASP would have caught this" is a counterfactual and this is exactly
+ where sponsor optimism would otherwise enter. The fixture corpus
+ itself is a nontrivial investment made *before* the fund/kill decision
+ it feeds — stated plainly here so M-1's denominator is honest — but it
+ is outcome-independent: the corpus, manifests, and cheap-arm results
+ retain full value for outside-in tooling even if RASP dies at this
+ gate. Because M-1 shapes the metric definitions, it is exploratory by
+ construction; therefore the confirmation cases are **withheld and
+ custodied before any M-1 execution** — the independent custodian
+ receives a sealed slice of the corpus (and of field incidents) that no
+ M-1 arm runs against and no metric/schema designer sees, with
+ per-fixture exposure status and provenance recorded; a **held-out
+ confirmation run** after M0 freezes definitions then validates the
+ thesis on genuinely unseen data, against a preregistered comparator —
+ the best cheap arm, with a preregistered minimum false-pass
+ improvement RASP must clear. (If pre-M-1 custody proves impractical, a
+ freshly collected post-freeze corpus is the fallback; splitting
+ already-seen cases after M-1 is not held-out evidence.) Exit: a measured
+ failure distribution and cheap-arm comparison that either fund the
+ build or kill/reshape it.
+- **M0 — architecture spike + schema draft + thesis decision.** The
+ spikes are governed by a **normative decision matrix** — for each
+ spike: core or optional layer, required success fact, which milestone
+ it unlocks, the consequence of failure (proceed / narrow / descope /
+ pivot / kill), the platform requirement (iOS, Android, either, both),
+ and its permanent owner. The load-bearing rows:
+
+ | Spike | Core/layer | On failure | Platforms |
+ | --- | --- | --- | --- |
+ | Host-semantic tree join (Fabric shadow node ↔ native view ↔ event descriptor ↔ root/window) + a diagnostic answer | **core** | blocks M1 | both |
+ | Identity, coherence, attestation conformance cases (§3.0, §3.1) | **core** | blocks M1 | both |
+ | Native-reachability producer (enumeration scope + residuals, point-resolution semantics, performance/legality assessment, deterministic higher-window and native-overlay negative fixtures) | **core** (the minimum core names reachability facts; universal — protocol integrity binds both theses) | **blocks M1 for both branches — the branch is chosen now, not after results**; the sole continuation is a *separately named, separately approved host-semantics-only pivot* available to either thesis, carrying no operability or completion language ever, with a reduced conformance obligation in which contributed driver-produced reachability facts are the only reachability source | both |
+ | Component/Fiber provenance correlation | optional layer | descope the provenance extension; M1 proceeds | either |
+ | In-process tier + ingress-identity propagation (§3.2) | layer (feeds `responderOperable`) | non-driver path disabled per platform; §3.4 contingency | per platform |
+ | OS-driver correlation (bounded spike) | layer (feeds `osDelivered`/release profile) | release profile unattainable per platform; recorded | per platform |
+ | Commit attribution under ambient noise | layer | descope to heuristic-informational (M2 rule) | per platform |
+ | Obligation-closure + coverage-domain feasibility (build-sealed manifest; partition attainment estimate on real generated apps — below an M0-set floor, the sealed tier is descoped from M3 language, not carried as aspiration) | layer (feeds the claims tiers) | claims layer restricted to "declared scenarios verified" | n/a (build-time) |
+
+ **M1's entry condition is stated in terms of the core technical rows
+ plus the cross-cutting security gate (threat/principal-model
+ approval).** M0 also builds one **executable proof-carrying vertical
+ slice** — one route, one button, one native overlay, one action
+ token, one attributed commit, one wrong-artifact case, every edge
+ fault-injected — which exposes schema incoherence faster than broad
+ interface design.
+ The in-process spike's conformance list explicitly includes both a
+ JS-responder-system gesture and a react-native-gesture-handler
+ native-arbitration gesture per platform — RN's "responder routing" is
+ architecturally plural, and timing cases alone do not cover the
+ gesture stacks; test the compatibility seam across one adjacent RN/Expo
+ SDK version — including the React DevTools plumbing if any is shared
+ (§3.6), since that protocol has its own version-skew history; decide
+ code ownership (Expo SDK module vs RN patch vs Fabric extension point
+ vs upstream) with the maintenance burden actually measured, not
+ assumed — inventory every hook the joins depend on (public / private /
+ upstream / patched / Expo-owned), test a **representative supported
+ SDK/RN window, not just one adjacent version**, produce an ownership,
+ staffing, and compatibility-cost model (with per-milestone size bands
+ for the decision brief), name the upstream-extension-point or
+ maintained-patch strategy, and define the narrower host-semantic
+ fallback explicitly if the minimum fact set proves to depend on
+ brittle Fiber internals — this is §5's moat claim being *priced*, not
+ presumed; run a **bounded OS-driver correlation spike** (XCTest/ADB
+ ingress-identity feasibility) so Expo does not invest through M2/M3
+ before learning `release-configuration-attested` is unattainable on a
+ platform (preferring stable upstream extension points throughout —
+ long-lived private Fiber/Fabric patches are a fallback carrying an
+ explicit maintenance budget); **preregister the worst-case operability
+ contingency** — if both the in-process and driver paths fail on every
+ platform, the named consequence is either re-scoping the completion
+ story to `semantic-development` + `nativePointReachable` under permanently
+ qualified language, or killing the reliability thesis; decided and
+ recorded at M0 like every other gate; and name the standing
+ cross-version conformance owner;
+ choose the governing thesis (Expo Agent reliability vs ecosystem
+ standardization — or both: when both signals clear, the branches run
+ concurrently with separate continuation ledgers and a shared-core
+ cost-attribution rule set in the brief, so neither branch's spend
+ subsidizes one that failed its own gate) and state kill/pivot
+ criteria — **selecting the standardization thesis requires the
+ written partner confirmation below; if partners report an existing renderer contract, the work
+ reframes as interoperability, not protocol leadership — and selecting
+ standardization also requires a protocol-governance decision:
+ Expo-owned de facto standard vs RN-upstream contract vs joint partner
+ governance, with contribution, compatibility, and deprecation policy
+ named — and review from the RN renderer owner, since tool-consumer
+ agreement is not a substitute for an upstream extension/versioning
+ commitment** (renderer-owner *technical* review on seam feasibility
+ and maintenance cost is sought under the reliability thesis too;
+ only the governance decision is standardization-specific) (the reliability thesis needs no partner input); freeze the M1–M3 metric
+ definitions and numeric floors **and preregister the held-out
+ experiment in full** (train/tuning vs held-out corpus split frozen
+ before implementation begins and **sealed under an independent
+ custodian**; unit of analysis; a **joint primary endpoint** — false-pass
+ rate at matched completion/obligation-resolution coverage, with every
+ abstention class (`unresolvable`/`unsupported`/`timed-out`) counted,
+ and a completion/resolution non-inferiority floor against the same
+ comparator, so a conservative verifier cannot buy its false-pass number
+ by abstaining on hard decisions; best-cheap-arm comparator; minimum
+ effect; cost secondaries; **the concrete accept decision scored per arm** —
+ for RASP at M2, obligation resolution over the acceptance truth
+ table; for each cheap arm, its own named assertion surface — all scored
+ against the same ground-truth labeling unit (fixture expected-fact
+ manifests plus adjudicated field incidents), so the false-pass
+ endpoint is computable without the M3 claims runner and the arms are
+ genuinely comparable rather than asserted so; the kill/pivot consequence if the
+ threshold is missed); publish the normative appendix (envelope,
+ acceptance truth table, fixture manifests, and one end-to-end worked
+ artifact — a snapshot fact, reachability fact, ingress receipt,
+ handler token, attributed commit, claim verdict, attestation tuple,
+ and coverage-ledger entry, plus the counterexample where one missing
+ join flips the verdict to `unresolvable`) plus a 1–2 page decision
+ brief for Expo/partner leadership; pick the real protocol name (§1). Landscape table validated with partners, including
+ explicit *written* confirmation of the §2.1 load-bearing assertion (a
+ two-question form — "does your tool consume a renderer-published,
+ versioned semantics contract today?" / "if one existed, would you?" —
+ so the confirmation is an artifact, not a meeting impression —
+ supplemented by a **renderer-owner inventory** — and the check of the
+ load-bearing assertion moves to *M-1 entrance diligence*, since if a
+ supported contract already exists, even partially, the cheap arms and
+ interoperability framing should know before the web prototype begins
+ —: what supported
+ inspection/semantics extension points RN itself already exposes, who
+ owns them, and what versioning promise they carry, since "our tool
+ does not consume one" does not by itself prove no contract exists;
+ the external documentation is pinned and archived for the decision
+ record); SwM + Callstack schema review sought. Exit (Expo-controlled only): the **core matrix rows** pass on both
+ platforms; the five ownership decisions are made *separately* —
+ code placement, program DRI, cross-version compatibility owner,
+ attestation owner, and corpus amendment authority (plus the M2
+ disagreement oracle named); thesis and protocol name decided;
+ thresholds frozen; appendix published. Partner review is scheduled,
+ not gating.
+- **M1 — semantics tree + snapshots + annotated screenshots** in dev
+ clients, iOS + Android, against the fixture corpus. **Entry condition:
+ the §3.5 threat/principal model is approved.** Perf is a frozen budget,
+ not a bare "measure": snapshot cost and observer effect (JS-thread
+ occupancy / frame-time share during capture) within the M0 floors, with
+ changed-set-driven serialization designed in from the start — Exact's
+ actual experience was that naive per-read full-walk JSON serialization
+ on the app's JS thread was a real problem that required a dedicated
+ incremental-serialization workstream to fix (LLP 0321 B1, still
+ flag-gated); RN apps are bigger and the fusion is costlier, so this is
+ a starting constraint, not an optimization. Exit: an agent completes a
+ 10-step task by refs only; expected-fact manifest conformance (positive
+ completeness); zero false passes on deterministic negative fixtures;
+ correct-`unknown` classification and observer effect meet the M0
+ floors; **negative security conformance tests pass** (unauthorized
+ reads, Host/origin + DNS-rebinding attacks, token scope, redaction,
+ production stripping); handle coverage (share of significant nodes with
+ stable handles) reported; mis-targeting rate *by ladder rung* reported
+ (the semantic-id-above-handles adaptation is confirmed with data, not
+ assumed — with the reversal preregistered: a rung-level result worse
+ than the Acto order flips the ladder back); the §3.0 attestation fixtures pass (wrong-project,
+ stale-bundle, different-config, mixed-HMR, process-restart all fail
+ closed); and a **cold-client discoverability fixture** passes — an
+ agent with no prior RASP exposure must discover and correctly use an
+ unfamiliar capability from the manifest and skill within a fixed
+ tool/context budget (the LLP 0365 lesson is behavioral, not just
+ mechanical: a correct manifest can still be badly surfaced).
+- **M2 — receipts + diagnostics + shadow calibration.** Exit: on the
+ fixture corpus, the agent diagnoses both a renderer-layout blocker
+ (clipped/zero-size) *and* a higher-window/host-overlay blocker via
+ `why_not_hittable` — semantic-only success is classified as suspect,
+ never passed; **attribution accuracy** measured on ambient-noise
+ fixtures (action-caused commits correctly attributed vs ambient commits
+ correctly excluded) meets the M0 floor — this gate is fixture-scoped
+ (runtime-synthesized input, where propagation is achievable); if the
+ floor proves unmeetable on driver paths, the defined outcome is
+ **descope, not kill**: attribution degrades to heuristic-informational,
+ the gate re-scopes to the surviving facts, and only failure on
+ fixture-scoped synthesized input kills (consistent with §3.2's
+ de-risking statement); completion rate non-regresses against M-1 (the
+ F2 ablation discipline); the **shadow-mode disagreement matrix** (RASP
+ vs outside-in tools on identical runs, explicitly bidirectional — cases
+ where the outside-in tool is right are the calibration gold) is
+ recorded; a **human-interpretation test** runs (developers/agents
+ shown evidence reports with degraded, contradictory, and qualified
+ results must make the intended ship/no-ship decision — honest
+ evidence that users systematically misread is still operationally
+ unsafe); and **every disagreement is adjudicated against a named
+ oracle** before RASP gates anything downstream; unexplained conflicts
+ remain `contradictory`/`unresolvable`, and any RASP false pass blocks
+ M3. **M2 exit is where the preregistered held-out experiment runs**:
+ RASP against the frozen held-out corpus, scored on the M0-preregistered
+ primary endpoint against the best-cheap-arm comparator and minimum
+ effect — clearing it is the M3 entry condition; missing it triggers
+ the preregistered kill/pivot consequence, not a threshold
+ renegotiation. The run includes **leave-one-evidence-family-out
+ ablations** (tree only; +reachability; +attestation;
+ +receipts/causality) so the smallest evidence set that materially
+ improves reliability is identified empirically rather than assuming
+ the architectural MVP is the empirical one. Scope note: M2's held-out arm tests the **evidence
+ protocol** (facts, receipts, diagnostics, obligations), explicitly
+ independent of the claims *product* M3 introduces on top of it.
+- **M3 — claims + `expo verify` (development scope).** Entry: the M2
+ held-out gate cleared; **a verification-grade input mechanism**
+ operational on at least one platform — the in-process tier is the
+ *preferred* one because mutation suites need controllable, repeatable
+ input, but if verification-grade OS-driver correlation lands first,
+ driver-based mutations satisfy this entry (the M0 worst-case
+ contingency fires only when *no* verification-grade mechanism exists
+ anywhere). One asymmetry is normative: input-to-handler correlation
+ does not supply the handler-to-commit edge `causes(...)` needs, so
+ **generalized "done" additionally requires, per platform and outcome
+ family, verification-grade expected-handler observation *and* the
+ same-action outcome/commit edge** — where only input delivery is
+ verification-grade, M3 ships claims and evidence reporting but does
+ not gate generalized "done" (the M0 matrix and M2 descope rule carry
+ the same distinction); and the **minimum scenario
+ model specified** (setup steps, navigation/scroll/input actions,
+ runner ownership and hosting — resolving OQ16 — settling semantics,
+ coverage-ledger semantics, and how an unavailable external driver
+ affects a verdict) before claims become a product commitment; and an
+ **effect-safety policy** in force — the claims runner executes real
+ presses, so before it may exercise consequence-bearing actions it
+ requires an attested fixture/test-account or effect-interception
+ boundary, capability and consequence authorization (the LLP 0271
+ boundary: provenance never authorizes effects; capability confinement
+ is load-bearing), trusted confirmation for destructive/costly
+ effects, reset/idempotency/cleanup semantics, and **fail-closed
+ verdicts when safe execution is unavailable** — with a
+ destructive-external-effect fixture that must be denied and can never
+ contribute to "done" (extending LLP 0278's fixture-providers-only generator contract — the
+ denial fixture itself is RASP-new). Exit: Expo Agent gates
+ "done" per the §3.4 certification profiles (`responder-operability`
+ for consequence-bearing gestures by default; `semantic-development`
+ only for labeled development checkpoints — semantic-only never
+ satisfies any profile), **scoped to the platform(s) where the
+ operability mutation suite has actually run**: with the in-process
+ tier on one platform, M3 is explicitly a one-platform pilot and its
+ "done" language carries that scope; generalized cross-platform claims
+ require the suite on each advertised platform; the claim-kind
+ distribution (`dispatches`-only vs `nativePointReachable` vs `responderOperable`
+ share) is tracked alongside the weakening rate;
+ and — because M2's held-out gate proves *protocol-fact* accuracy, not
+ the reliability thesis — **M3 exit adds a preregistered
+ agent-in-the-loop held-out gate before any M4 adoption**: actual Expo
+ Agent generation/repair tasks run against RASP and the best cheap arm
+ under matched budgets, the final ship/repair/completion decisions are
+ scored against an independent product-outcome oracle with abstentions
+ counted, using both a frozen comparator (reproducibility) and a
+ contemporaneous best-current comparator (honesty); if that gate cannot
+ be run, the governing thesis narrows from "Expo Agent reliability" to
+ "truthful observability substrate" and says so. Claims face mutation
+ tests: wrong-handler dispatch, unobserved required claim, correct semantic dispatch with a witnessed negative reachability fact
+ (the probe reports the point does not reach the target), an
+ unavailable reachability probe (must yield `unresolvable`, not
+ `passed`), contested exclusive-input lease (must yield `unresolvable`),
+ the §3.2 substitution cases (intended event dropped + sole ambient
+ substitute; delayed prior event as sole ingress — both `unresolvable`),
+ the composition failure (a witnessed positive reachability fact +
+ semantic token success + broken responder path — must not pass
+ `responderOperable`, **and must fail
+ the default product-completion decision even when no stronger claim was
+ manually authored**, via the consequence-bearing auto-escalation), the
+ cross-action substitution (in-process operability proof for action A
+ plus unrelated OS delivery for action B must not compose into
+ `release-configuration-attested` for either), and missing driver evidence
+ for `osDelivered` — each must fail the gate. Contributed driver evidence is consumed only after the §3.2
+ adversarial cases exist. The tethered release-verification artifact
+ (§3.5) is defined here; until it exists, all claims language is scoped
+ to "verified in development," never "shipped."
+- **M-Go — Expo Go gate (only if Go delivery is pursued).** Cross-project
+ isolation conformance (evidence never crosses project, session, or
+ device incarnations inside one Go host), per-project capability
+ advertisement, and the Go payload budget (binary size, startup) met.
+ Until this gate passes, the delivery promise is dev clients only.
+- **M4 — ecosystem adoption (outcome, not exit-gate), scored per
+ thesis.** The Expo-controlled deliverables: Expo MCP local tier rebuilt
+ on RASP, the conformance suite + wire corpus published, cross-version
+ conformance operating under its M0 owner, and the physical-device
+ transport under the §3.5 threat model. Terminal scoring is
+ thesis-specific: under the **reliability thesis**, Expo MCP + Expo
+ Agent on RASP with measured false-success reduction is success even if
+ both partners decline — but the strongest reliability claim available
+ is **`release-configuration-verified`**: at least one end-to-end
+ `release-configuration-attested` run (attested sibling + operability
+ evidence) on each platform it covers, never phrased as shipped-app or
+ store-byte language (§3.4); without that, terminal success is
+ honestly narrowed to development verification.
+ Under the **standardization thesis**, success requires time-bounded
+ external adoption evidence (at least one non-Expo consumer or
+ contributor in production within the window set at M0); absent that,
+ the honest outcome is a *pivot to Expo-internal infrastructure*,
+ recorded as such rather than relabeled success. After M2, calibration
+ becomes an operating system, not a launch gate: a standing
+ **false-success bounty** turns every field false pass or
+ RASP/outside-in disagreement into a minimized replay, an
+ expected-fact manifest, an evidence-gap classification, and a corpus
+ amendment decision. If Expo adopts the
+ architecture but trims the experimental apparatus, the
+ **non-negotiable minimum** is: zero-false-pass deterministic
+ negatives, the sealed held-out gate, fail-closed attestation, and the
+ schema-linted result-presentation rule (a qualified result rendered
+ as an unqualified pass defeats the other three) —
+ without those four, the honesty claims in this document are
+ unenforced and should not be repeated.
+
+## 5. Risks and objections
+
+- **"Fabric internals are unstable across RN versions."** True; this is
+ why M0 decides placement/ownership, tests a representative supported
+ SDK/RN window (not just an adjacent seam), prices the burden with an
+ ownership/staffing/cost model, and names a standing conformance owner
+ before the schema freezes. The
+ implementation lives with the party that already absorbs RN version
+ churn for the ecosystem (Expo SDK releases). That maintenance burden is
+ also the moat — it is the cost that keeps outside-in tools outside.
+- **"The runtime tree will certify things users can't do."** It will —
+ that is the Partitime false-positive class, and rev 1 of this document
+ had exactly this flaw. The design answer is §3.0/§3.2/§3.3/§3.4: native
+ reachability is a separate, native-authority fact; input delivery is a
+ contributor fact with a trust model; claim kinds declare evidence
+ minima; and the operability gate cannot be satisfied semantically. The
+ M2/M3 exits test this directly.
+- **"Partners may not adopt it."** The fallback is stated in M4 and is
+ still good: Expo MCP and Expo Agent alone justify the layer, and
+ coverage-explicit runtime observation versus outside-in
+ reconstruction is a visible quality difference that creates adoption
+ pressure. M0 asks for review, not commitment; the
+ conformance suite makes adoption verifiable when it happens.
+- **"Perf overhead of maintaining and serializing the tree."** A real
+ risk, per Exact's record (see M1). Dev-mode-only delivery bounds the
+ blast radius but does not excuse the design work; the observer effect is
+ itself a frozen M1 budget — and includes the gesture-claim
+ reachability probe's per-iteration latency, since a probe developers
+ route around shows up in the downgrade ledger late rather than early.
+ The conformance corpus and expected-fact manifests are themselves
+ version-coupled artifacts re-baselined every SDK cycle; that carrying
+ cost is part of the moat's price and sits inside the M0 cost model.
+- **"React's tree is emergent; identity is weaker than Contract's."** Yes
+ — refs in a reconciled tree need care around keys, flattening, and
+ virtualization. This is the hardest open design problem (§6) and the
+ reason M0 prototypes identity before anything freezes.
+- **"This could be a solution looking for a problem."** M-1 exists to
+ answer exactly that with a measured failure distribution before build
+ investment; the kill/pivot criteria at M0 are the honest exit.
+
+## 6. Open questions
+
+1. **Ref identity across reconciliation** — what composite survives keyed
+ remounts, flattening, and virtualized recycling without silently
+ retargeting? (Shared with Exact: LLP 0321 OQ10's slot-vs-item problem.)
+2. **Where the semantics layer lives and who owns compatibility** — Expo
+ SDK module, Expo-maintained RN patch, Fabric extension point, or
+ upstream RN core; what RASP-version-per-SDK promise is made; how dev
+ clients negotiate version skew between installed runtime and agent
+ tooling. (M0 decides placement and names the owner; the promise needs
+ ongoing governance.)
+3. **Receipts under concurrent React** — how far can an action identity
+ propagate through the commit pipeline (transitions, deferred updates)
+ before facts degrade to `heuristic`? §3.2's de-risking statement says
+ the layer survives a pessimistic answer; the question still bounds v1
+ claims.
+4. **Driver ingress correlation** — can XCTest/ADB paths carry a true
+ ingress-propagated identity (or a shared exact event key) on either
+ platform — and can that one identity survive the full path through
+ responder/gesture handling to the expected action token and resulting
+ commits? If not, `armed-bracket` (never verification-grade) is the
+ ceiling for contributed input evidence, and `responderOperable` is only
+ achievable via runtime-observable realistic input. What input
+ classes the app runtime can actually witness during a diagnostic
+ lease, per platform, sets the same floor.
+5. **Snapshot coherence contract** — what skew bounds are guaranteed
+ across React commit, mount transaction, native animation/scroll, window
+ state, and screenshot capture, per platform?
+6. **Claims registration binding (v2)** — what associates a claims
+ declaration with a mounted instance across HMR remount, `memo`, and
+ HOC wrapping; and what fact projection, per-instance scope, and
+ redaction model makes state-guarded claims safe and executable?
+7. **Library claims** — do RN component libraries ship claims for their
+ own components (the analog of `@exact/facet-contract` components
+ carrying contract blocks), using claim-local handles, and who owns the
+ app-level registry?
+8. **Organizational ownership** — which Expo team owns RASP (the Expo
+ Agent team if the M0 thesis is reliability; a platform/SDK team if
+ standardization), distinct from M0's *code*-placement decision?
+9. **Production diagnostics** — is there a redacted, opt-in production
+ tier (crash-adjacent semantics snapshots), or is that a separate
+ proposal? (The M3 tethered release-configuration artifact is the
+ nearer-term answer for release-build verification.)
+10. **The completeness oracle beyond fixtures** — expected-fact manifests
+ define faithfulness on the corpus; what defines a *faithful* (not
+ merely useful) semantics tree on arbitrary real apps, and who owns
+ extending the manifest set as RN grows new constructs?
+11. **Shadow-mode adjudication ownership** — when RASP and an outside-in
+ tool disagree on the same run, which named oracle (and which team)
+ decides who was right, so M2's adjudication requirement has an
+ owner rather than a queue?
+12. **Fixture-corpus governance** — after M-1 the corpus becomes the de
+ facto oracle for M2 adjudication and M3 mutation tests, and partner
+ tools get scored against it; that is standards-body weight, and it
+ needs an assigned owner and amendment process.
+13. **Semantic-id lifetime contract** — what exactly resets a "root
+ incarnation" in RN terms (reload, Fast Refresh, Android activity
+ recreation), since that determines how often the ladder's top rung
+ drops out from under a long-running agent session?
+14. **Attestation ownership and the deferred exact-byte design** —
+ which component owns the target/artifact attestation protocol
+ (§3.0); what attests the release-configuration sibling's
+ shared-input chain (EAS build inputs → sibling); and does the
+ deferred exact-byte design (dormant authenticated channel, or
+ two-artifact attestation of shared payload sections plus wrapper)
+ ever get built, with what threat/size/privacy budget?
+15. **Registration under Fast Refresh** — can v1 component-claim
+ registration survive ordinary Fast Refresh without either silently
+ rebinding or creating intolerable author friction? (Route-scoped
+ claims dodge this; component registration does not.)
+16. **Claims-runner hosting** — do claims execute in CI in v1 (dev
+ client on simulator, driven how?) or only interactively under Expo
+ Agent — and does the CI answer reintroduce a partner-driver
+ dependency that the in-process tier was meant to avoid?
+17. **Dev-loop survival under attestation-epoch churn** — Expo Agent
+ iterates under Fast Refresh, and fail-closed attestation binds to
+ the module/HMR epoch: what fraction of dev-loop verification
+ attempts survive, and is the sanctioned workflow "fresh epoch, then
+ verify"? M1's design confronts this rather than discovering it.
+
+## Appendix: Revision History
+
+This appendix is corpus-internal review provenance; it is dropped from
+any externally shared rendering of this document. All revisions
+2026-07-21, one entry per super-refine round response (newest first).
+
+- **rev 23** (2026-07-22) — round-22 review response (Fable READY / Codex NOT READY): the standardization branch's demand gate operationally attached — **executable partner consumption, gated pre-M1** (at least one non-Expo design partner runs the schema/ledger and conformance corpus over its own recorded facts via a provided adapter and names an integration owner/timeline or ships a minimal adapter), with any native M1/M2 spend absent that gate explicitly labeled Expo-funded option value; the native-reachability failure consequence unified across all three locations — reachability is universal minimum core, failure on either required platform **blocks M1 for both branches**, and the sole continuation is a separately named, separately approved host-semantics-only pivot available to either thesis with contributed driver-produced reachability as the only reachability source and no operability language ever; branch concurrency stated (both may run when both signals clear, with separate continuation ledgers and a shared-core cost-attribution rule so neither subsidizes a failed branch); the §2.2 renderer-authority sentence narrowed (renderer state for renderer-owned clipping/sizing/stacking; native-host evidence for higher windows and chrome); the Phase A wording refined (consumes only that some press-family handler ran; the tap receipt's optional action/owner provenance is unconsumed); the `writeContractOk` scoping clarified (raw field exists for dev-registered instances generally; the verdict gate becomes contract-block-scoped through the verify-claims operation); and "whoever publishes first defines the schema" softened to an early coordination point whose adoption is a governance outcome.
+
+- **rev 22** — round-21 review response (Fable READY / Codex NOT READY; the rnw-conditionality tension and the "those three" miscount found by both): the thesis split closed over the whole decision contract with a normative **thesis-by-milestone matrix** maintained as a content-addressed decision ledger — per gate/deliverable, required/optional/inapplicable under each thesis; a failed reliability gate kills only the reliability branch and never blocks a standardization program with its own demand signal; universal protocol-integrity requirements (zero-false-pass negatives, envelope conformance, fail-closed attestation, presentation rule) bind both branches while cheap-arm superiority and false-pass improvement are reliability-only economics; the react-native-web arm is a deliverable of the funded reliability build arms (conditional on M-1A), and the M0 vertical slice passes an unavailable optional attribution layer by representing the absence faithfully (absent/heuristic edge, `unresolvable` dependent verdict) rather than blocking the core; the external-contract validation scheduled once consistently (M-1 entrance archived-source + renderer-owner inventory; M0 written partner revalidation; partial-contract discovery auto-reframes to interoperability before any thesis-specific build approval); the summary paragraph's "exact-set" regression fixed (exact-set error enforcement for authored block clauses; asymmetric diagnostics for optional headers; syntax/name/purity validation of guards); "without those three" corrected to four; the Review-resting gloss softened to "closest available resting status"; the external-rendering drop extended to the Exact-defect parentheticals in the claim-kind bullets; fact-gap cards gain a producer-feasibility field (supported hook / upstream-plausible / maintained patch / unknown); the distribution-channel claim qualified to Expo-managed channels, not RN renderer ownership; and the §3.7 daily-use row carries its author-reported qualifier.
+
+- **rev 21** — round-20 review response (Fable READY / Codex NOT READY): the M-1 gates made **thesis-specific and preregistered** — the reliability build arms (react-native-web prototype) gate on the addressable-prevalence threshold while the standardization arm (schema/ledger + partner appetite) proceeds on its own demand signal (entrance-diligence contract confirmation, concrete partner interest, compatibility-cost ceiling), so low Expo Agent failure prevalence kills the build arms without silencing standardization evidence and neither thesis can be revived post hoc; the Decision block's brief gains a **mandatory corpus-governance plan** for the sensitive M-1 data (default synthetic/internal or expressly authorized traces; source authority, consent basis, minimization, de-identification and secrets scanning, raw-data access and audit logging, retention/deletion, partner-export rules, conflict/recusal) with external or operationally independent custody/adjudication under a CEO sponsor, the apparatus itself as a ceilinged line item excluded from continuation arguments, and a pitch claim-strength fidelity check at second approval; the calibration precision items fixed (action-header writes are set-compared — under-declaration errors, over-declaration warns — with "exact-set enforced" reserved for authored block clauses; the accidental headerless gate scoped to contract-bearing components evaluated by the verify-claims operation); the schema-linted result-presentation rule joins the non-negotiable minimum (a qualified result rendered unqualified defeats the other three); M-1 classification reports *protocol-relevant fact gap* and the stricter *RASP-addressable* (named plausible producer + decision relevance) as separate figures; and each field incident is packaged as a privacy-preserving **fact-gap card** so partners adjudicate without receiving raw traces. Exact-side regression tests for the surfaced defects join the residue list.
+
+- **rev 20** — round-19 review response (Fable READY / Codex NOT READY): the last post-result escape hatch pinned — native-reachability core failure **blocks M1 under the reliability thesis, with the branch chosen now**, and a host-semantics-only continuation is a separately named pivot requiring fresh approval that carries no operability or completion language, ever; the switcher-incident claim narrowed to what the record supports (semantic-mode tooling in use at the time; the LLP 0326 benchmark ran other tasks, not that incident); the M-1 addressability classifier bound to a content-addressed fact-table version with retroactive reclassification of the funding gate forbidden (M0 changes are a new classifier version, never a rescore); the status-tense wording fixed (sits at `Draft` until the loop completes; resting status `Review`); the `has`/`present` syntax relationship stated (has(selector) is the authored syntax; present names the predicate a has witness proves); the reachability scope wording made platform-contingent (own-window/keyboard visibility are M0-produced scope facts, not guarantees); a standing post-M2 **false-success bounty** added (every field false pass or disagreement becomes a minimized replay + manifest + evidence-gap classification + corpus amendment decision); and the corpus-internal §3.4 citations expanded to the seven implementing files so the paragraph's correctness is independently auditable.
+
+- **rev 19** — round-18 review response (Fable READY / Codex NOT READY): the Decision block re-scoped honestly — the immediate ask is authorization to *prepare* the populated M-1 brief (numeric per-item and aggregate ceilings, sampling frame, kill/continue thresholds, named sponsor/custodian/adjudicator), with M-1 *execution* requiring a second approval of that brief; M-1 gains a blinded, stratified, denominator-bearing **ecological sample** of ordinary Expo Agent tasks (prevalence and base rates, kept separate from the adversarial corpus) and splits into **M-1A** (ecological baseline + cheap arms) gating **M-1B** (schema/ledger + react-native-web prototype) on a preregistered addressable-prevalence threshold; a **native-reachability core row** joins the M0 matrix (producer, enumeration scope and residuals, point-resolution semantics, performance/legality, deterministic higher-window/native-overlay negative fixtures — blocks M1 or explicitly narrows the core and thesis); **waiver semantics closed** (a waived consequential obligation blocks sealed/generalized verdicts or schema-forces `qualified`, waiver share carries an M0-set ceiling, each waiver records approver/rationale/artifact, and a mass-waiver mutation must fail); the **disclosure/retention axis** added to the normative envelope enumeration (display/persist/replay/export composed most-restrictive-wins — witnessed-but-forbidden is not an availability reason); the capability surface rebuilt on LLP 0365's actual design — an **authored intent/capability and GAP authority** (aliases, owner, expiry, remedy) joined bidirectionally to the generated registries, because a generated index cannot name absent capabilities; the precision items fixed (reachability captured immediately before dispatch against pre-action state; same-level *other-window* occlusion; visible:true on all Phase A selectors with interactable:true only from the state-visible lowering; the surviving "entirely omitted destructive action" mutation reworded to implemented-but-unmanifested); M0 gains the executable proof-carrying vertical slice with fault-injected edges; the load-bearing landscape check moves to M-1 entrance diligence; "react-native-web joins are easy" demoted to a timeboxed feasibility hypothesis; the opaque-duplicate-handle coverage fixture added (ambiguous targeting requires complete scope or sealed uniqueness); the partition-attainment estimate gets its decision rule (below an M0 floor the sealed tier is descoped from M3 language); the ladder adaptation's reversal is preregistered; and the react-native-web arm gains a mechanical sunset (code not carried past the M0 schema freeze).
+
+- **rev 18** — round-17 review response (Fable READY / Codex NOT READY): the outbound lifecycle made LLP 0001-conformant without local redefinition — the document remains at `Review` as the author's position, moves to `Accepted` only when a named Expo decision authority approves a precisely scoped program with the decision artifact linked (the author alone never advances it), and a **Decision requested** block scopes the immediate ask to M-1 only with M0 gated on M-1's result and M1–M4 each requiring fresh approval (nothing in the status ever implicitly endorses the full program); the "adopts or adopts" typo fixed in the same passage; the two operational-experience superlatives qualified as author-reported (daily agent use; highest-leverage diagnostics); the M0 spike matrix gains an obligation-closure/coverage-feasibility row (claims tiers restricted to "declared scenarios verified" on failure; partition-attainment estimate on real generated apps reported); the conformance suite gains a result-presentation case (a qualified pass rendered unqualified fails conformance — Expo's linting cannot bind partner surfaces); the profile-level result type is closed and includes normative `qualified`; §3.4 terminology normalized (`present` single term; `unchecked` added to the live state list; Phase A's visible described as geometry-visible selection plus interactable expectations); a proof-directed evidence-acquisition planner and the production-vs-fusion placement rule added to §3.3; and RN renderer-owner technical review extended to the reliability thesis (only the governance decision is standardization-specific).
+
+- **rev 17** — round-16 review response (Fable READY / Codex NOT READY): the sealed-verdict quantification gap closed — sealing closes declarations, not the behavioral space they quantify over, so each obligation now carries a **coverage-algebra domain model** (declaration identity x route-parameter/data partitions x precondition/state fingerprints x instance quantifiers x scenario inputs; coverpoints, finite partitions, cross-coverage, waived and unreachable bins, residuals), the sealed verdict is available only over sealed finite covered partitions (or an accepted equivalence argument), any unbounded/opaque/uncovered dimension restricts the result to "declared scenarios verified" with an explicit residual, and a same-token-passes-one-partition-fails-another mutation must not produce a sealed pass; the fourth surfaced Exact defect recorded (runtime `state hidden` is unsatisfiable while Phase A lowers `hidden` to visible-filtered absence); the `reachesTarget` vocabulary residue in the M3 fixtures replaced with witnessed reachability-fact language; the §3.4 Exact-baseline paragraph marked corpus-internal (dropped from external renderings, like the appendix); a dev-loop attestation-epoch-churn open question added (Fast Refresh survival rate; "fresh epoch, then verify"); the M0 normative appendix gains the end-to-end worked artifact with its missing-join counterexample; profile names, residual qualifications, and prohibited completion phrases become schema-generated and linted in every consuming surface; and M2 gains a human-interpretation test (evidence reports with degraded/contradictory/qualified results must produce the intended ship/no-ship decision).
+
+- **rev 16** — round-15 review response (Fable READY / Codex NOT READY): the closure authority's limit made explicit — **the generated manifest closes implementation, not intent** (the generation authority cannot emit an obligation for a feature absent from its own inputs), so the verdict tiers: the manifest-scoped result is "build-sealed implementation obligations verified on ⟨platform/profile⟩" with unqualified "done"/"product complete" prohibited at every tier, and the stronger "sealed specification satisfied" requires an independently authored, frozen pre-build acceptance manifest attested alongside the artifact-derived one and reconciled bidirectionally (Expo Agent must never silently rewrite the standard its own output is judged by); the omission mutation restated satisfiably (an *implemented* destructive action omitted from the manifest must block; intent-level omissions are caught only at the specification tier); **an agent-in-the-loop held-out gate added at M3 exit** before any M4 adoption — actual Expo Agent generation/repair tasks vs the best cheap arm under matched budgets, final ship/repair decisions scored against an independent product-outcome oracle with abstentions counted, frozen plus contemporaneous comparators, and the thesis honestly narrowing to "truthful observability substrate" if the gate cannot run; the three Exact-baseline precision items fixed (the switcher hit was claimed by a topmost native overlay, not an ancestor view; Phase A's has/missing emits visible-filtered selectors so only the monitor approximates raw presence; guarded `when` emission is whitelist-based); the react-native-web arm operates under the effect-safety boundary from its first effectful run; M1's entry restated as core technical rows plus the cross-cutting security gate; the M2 held-out run gains leave-one-evidence-family-out ablations so the empirical MVP is identified rather than assumed; the schema split into a renderer-neutral evidence core with separately versioned Expo Router/EAS/Expo Agent extensions; the LLP 0278 effect-safety gloss corrected (extending its fixture-providers-only generator contract; the denial fixture is RASP-new); the disclosure-axis sentence tagged per LLP 0374 W5; and the two revision-churn garbled sentences repaired.
+
+- **rev 15** — round-14 review response (Fable READY / Codex NOT READY): semantic-structure claims gain a **scope-completeness facet** (`complete`/`partial`/`opaque`/`unavailable`) — positive `has` witnesses pass under any coverage, `missing` passes only under complete coverage (no-match under incomplete ⇒ `unresolvable`), exact `count` requires complete coverage else reports a lower bound, with Exact's Phase A zero-match-passes-missing rule named as the unsafe precedent and four coverage fixtures added; the closed obligation universe gains its **closure authority** — generalized "done" is scoped to Expo Agent–generated Expo Router apps under a build-sealed, content-addressed, proof-carrying obligation manifest (codegen emits; providers contribute bounded submanifests; runtime reconciles; the attested artifact binds the digest; dynamic escapes disqualify; closure feasibility is an M0 experiment and M3 entry gate), with the honest statement that arbitrary RN has no closure authority (Exact's own navigation analysis marks React edges statically unclosable; `exits only` is authored, not inferred) and brownfield keeps "declared scenarios verified" permanently; the M3 causal asymmetry made normative — input-to-handler correlation does not supply the handler-to-commit edge, so generalized "done" requires, per platform and outcome family, verification-grade expected-handler observation *and* the same-action commit edge, and driver-only-input platforms ship claims and evidence reporting without gating generalized done; the expected token named in `responderOperable`/`causes` syntax (or bound to one unambiguous sibling `dispatches(token(...))`) with missing/ambiguous identity ⇒ `unresolvable`; non-loopback transports require authenticated encryption with channel binding and replay protection, and destructive confirmations are host-owned and bound to operation/target/principal/descriptor; the surviving §5 "lossless-vs-lossy" replaced with coverage-explicit phrasing; the ambient-same-coalesced-commit `causes` fixture added (fail closed when per-change provenance cannot distinguish); the probabilistic-evidence tier recorded as a rejected alternative (substitution is about identity, not frequency); custodian/adjudicator independence extended to evaluated tool vendors; the event-trace operation named as Exact's identity-minting op (an ordinary semantic tap does not mint); the dataflow-inspector gate reference corrected to the `writeContractOk`/`agreement` fields; Expo Router layout/group routes added to the route-instance conformance cases; the react-native-web arm given a preregistered output contract; probe per-iteration latency and corpus re-baselining carrying costs added to the M1 budget and M0 cost model; and the Revision History appendix marked corpus-internal (dropped from external renderings).
+
+- **rev 14** — round-13 review response (Fable READY / Codex NOT READY): generalized product "done" gains a **closed obligation universe** — a revision-bound obligation-discovery algorithm computes the denominator (significant affordances/action tokens, reachable routes and task-critical edges, consequence classes, required outcomes; the LLP 0085 `exits only` precedent generalized), every obligation is exercised-and-passed or waived under audited policy, unknown/opaque/unvisited consequential items block, four omission mutations must block, and absent the closed universe the result is named "declared scenarios verified" with completion language prohibited; `responderOperable` explicitly certifies input plumbing, not product correctness, and a v1 **`press(x).causes(assertion)` causal-postcondition claim** is added (expected token + same-action `resulting-commit-attributed` join + assertion against the attributed state; provider/domain outcomes need authoritative mutation receipts per LLP 0278 or return `unresolvable`; six outcome fixtures) with consequence-bearing gestures requiring it for "done"; M3 entry gains an **effect-safety policy** (attested fixture/test-account or effect interception, capability and consequence authorization per LLP 0271's provenance-never-authorizes boundary, trusted confirmation for destructive effects, reset/cleanup semantics, fail-closed verdicts when safe execution is unavailable, and a destructive-external-effect fixture that must be denied); attestation made **compositional for the Expo artifact model** (native build, embedded and activated EAS Update identity + JS hash, asset graph, config/plugin inputs, runtime compatibility, HMR epoch, per-component invalidation, OTA activation/rollback/mixed-update/asset-mismatch fixtures) with the trust boundary stated (measured/signed detects mismatch among trusted participants; it is not device-integrity attestation); the literal claim names `nativePointReachable`/`responderOperable` adopted throughout; the precision pass landed (iOS probe enumerates strictly higher window levels in-scene with same-level/cross-process occlusion and DEBUG-only private-hook distribution legality as residuals; the staleness "role" field is the serialized element type/original tag; navigation receipts carry matched-declared-edge metadata, not an interaction-provenance join; DOM fallback = no handler observation; the 0374 attribution corrected to semantic-path-only ingress minting with commitId minting as 0374's flagged new work; the security-posture citation moved to docs/agent-api.md + the origin RFC; the Workstream E Related gloss re-pointed to §3.1 as pattern precedent); the Argent row's platform-scope inconsistency with LLP 0320's source notes recorded in the table; the evidence report upgraded to a two-axis verification manifest (proof strength × obligation coverage) so negative space is visible; standardization-thesis selection adds RN renderer-owner review; "lossless" softened to coverage-explicit; and the non-negotiable minimum moves onto the M-1 pitch's first page.
+
+- **rev 13** — round-12 review response (Fable READY / Codex NOT READY; the `present` mislabel flagged independently by both): the interaction-evidence levels split three ways per code — direct semantic `press` witnesses some press-family handler; direct `change` witnesses **value injection/commit only** unless separately joined to handler evidence (Exact's type operation forcibly reports `dispatched: true` after commit even when the dispatcher found no Change binding — the third Exact defect this loop surfaced, recorded as residue); the DOM fallback witnesses event injection only — and RASP receipts carry distinct `valueCommitted`/`handlerObserved`/`nativeIngressObserved` fields instead of one overloaded boolean, with the react-native-web arm barred from promoting injection-only paths to `semantic-wired` and a no-Change-handler fixture proving the distinction; the provenance import restated as LLP 0271's *normative* model in full while naming what Exact actually ships today (taxonomy, hostile-ordering helper, hand-annotated tree slice; taint inference and per-span provenance deferred; coverage is new work); the `state` parenthetical fixed (`enabled`/`disabled`/`checked` live in Exact; presence via `has`/`missing`; `present` is RASP's term motivated by the conflicting `visible` implementations); the `semantic-development` table cell corrected to include the native point-reachability probe for gesture claims with probe-absent ⇒ `unresolvable`; the held-out custodian and incident adjudicator placed explicitly outside the sponsor's reporting chain (partner or external reviewer preferred — with a CEO sponsor, "non-author" alone buys no independence); the per-arm accept decisions scored against one shared ground-truth labeling unit (fixture manifests + adjudicated field incidents); verdict diagnostics generated as minimal-cut explanations from the proof graph; landscape sources pinned and archived with the M-1 pitch rather than at execution; and the literal claim names (`nativePointReachable`, `responderOperable`) stated as preferred, not merely candidates.
+
+- **rev 12** — round-11 review response (Fable READY / Codex NOT READY; the provenance subset flagged independently by both): content provenance rebuilt to LLP 0271's shipped model *in full* — separate source-authority and authorship/intent axes, the complete eight-class vocabulary (`app`/`system`/`user`/`other-user`/`third-party`/`external`/`generated`/`data-unknown`), conservative most-hostile-wins effective-class derivation, laundering fixtures covering the previously omitted classes and conflicting axes, and the normative 0271 invariant that provenance is advisory and can never authorize effects or relax capability/consequence policy (capability confinement is the load-bearing layer); the `visible` misassignment fixed — `present`/`rendered` is the strongest semantic-only claim, and `visible` is a proof-graph claim over committed layout, viewport/clipping, mounted-native geometry, opacity/transform, and declared occlusion scope with residuals and six negative fixtures, with Exact's own incoherent precedent (Phase A lowers visible to interactable; the witness treats presence as visible) named as the cautionary tale; the dataflow inspector's accidental de facto gate on headerless actions stated (declaredWrites:[] exact-compare folded into the agent-facing ok) with RASP's authored-only rule made explicit; the DOM fallback's event-injection-only proof level stated, with the react-native-web arm required to expose which level it witnessed and demoted to a disposable, non-normative vocabulary prototype that satisfies no native gate (doubling as the token-codegen ergonomics pilot); M4's terminal wording corrected to `release-configuration-verified`; the scope-note lifecycle aligned with LLP 0001 (Superseded on adoption with a linked authority; Withdrawn on rejection without one); the capability surface rephrased to LLP 0365's relevant discovery shape with authority provenance and freshness obligations; the semantic id carried as {semanticId, lastSeenFingerprint} so slot reuse and handler/trust changes fail closed for snapshot-bypassing callers; the M0 preregistration names the concrete accept decision scored per held-out arm (RASP: obligation resolution over the acceptance truth table; cheap arms: their own named assertion surfaces); `userOperable` joins the M0 rename list (`responderOperable` candidate) under the naming-tracks-proof-strength rule; the M-1 pitch names actual people/teams for the three independence roles and the M0 brief costs the trimmed-to-minimum variant as a first-class option; and selecting the standardization thesis now also requires a protocol-governance decision (Expo-owned vs RN-upstream vs joint partner governance, with contribution/compatibility/deprecation policy).
+
+- **rev 11** — round-10 review response (Fable READY / Codex NOT READY): M0's incompatible exit semantics fixed — the spikes are governed by a normative per-spike decision matrix (core vs optional layer, required success fact, unlocked milestone, failure consequence, per-platform requirement, permanent owner), the load-bearing rows are tabled (host-semantic tree join and identity/coherence/attestation conformance are core and block M1 on both platforms; component/Fiber provenance is an optional layer whose failure descopes the extension; in-process ingress, driver correlation, and attribution each carry their own per-platform consequences), M1's entry is stated solely in core-row terms, the M0 exit names the five ownership decisions separately (code placement, program DRI, compatibility owner, attestation owner, corpus amendment authority, plus the M2 disagreement oracle), and the in-process spike's conformance list gains JS-responder and react-native-gesture-handler arbitration gestures per platform; the three M-1 independence roles (sponsor/DRI, corpus custodian, incident adjudicator) are provisionally named before any M-1 execution; the scope note states this document can never become `Implemented` in LLP 0001's sense and moves to `Superseded` with a link to the Expo-owned authority once Expo decides; a `state` claim kind joins v1 (live in Exact today); the claim matcher is renamed `token()` to disambiguate from the `action()` authoring wrapper; public naming is committed to track proof strength (`nativePointReachable` as the M0 candidate for `userReachable`); a semantics-provider extension (LLP 0321 Workstream E pattern) is added for custom native components/brownfield/WebViews under producer identities and evidence ceilings with `opaque` as the default; the capability surface commits to the full LLP 0365 shape (intent aliases, runnable examples, anti-patterns, GAP entries) plus a declared schema-version skew answer; the release-configuration profile gains the instrumented-vs-uninstrumented differential run as perturbation-bounding evidence; the monitor semantics are stated precisely (per-invocation ⊆ vs static exact-set; "no observed write-set evidence"); the react-native-web arm is promoted to a named M-1 deliverable with its own exit; the weakening-rate metric gets a decision attached (token-coverage floor or explicit codegen-property scoping at M0); and the switcher-clicks example gains its concrete symptom sentence.
+
+- **rev 10** — round-9 review response (Fable READY / Codex NOT READY): the profile↔mechanism contract made a normative table (per profile: input mechanism — either / driver-required — same-action join grade, and the consequence of unavailability), with profiles expanding to property-tested declarative proof obligations (counterexamples for monotonicity, cross-action substitution, missing evidence, insufficient-fact composition) and `osDelivered` explicitly in the release profile's expansion; M3's in-process requirement clarified as a controllable-mutation-mechanism preference — verification-grade driver correlation satisfies the entry too, and the M0 worst-case contingency aligned to fire only when no verification-grade mechanism exists anywhere; the held-out experiment made genuinely held out — confirmation cases are withheld and custodied *before any M-1 execution* with per-fixture exposure/provenance records, designers never see them until the M2 run, and a fresh post-freeze corpus is the named fallback; route-scoped claims given route-instance identity (declaration = pathname; evaluation and ledger bind to app/root incarnation + router entry key + mounted-route incarnation + params identity, with same-path-different-params, retained-inactive-entry, and multi-root conformance cases); the navigation precedent corrected from "specification only" to its actual three levels — static navigation-declaration checking ships (navigation-contracts.ts), live settled-route receipts against declared edges ship and attach to interactions (navigation-receipts.ts), and only the propagated action-token→transition join is new; the Contract mechanism descriptions tightened (no-write-obligation wording covers header *and* block-clause absence; guarded `when` live only over live-capable inner clauses; "Phase A live verification" split into read-only assertions and effectful interactions); §5's version-skew summary updated to the representative-window + cost-model mitigation; OQ9/OQ14's rev-8 naming/model residue fixed; the LLP 0278 Related gloss names the landed clause forms instead of quantifying over Part A; the weakening-rate metric given an M1 static proxy with the true rate starting at M3; the minimum viable core surfaced in §1; the §2.1 DevTools-protocol sentence added (closest existing thing; fails the bar on every relevant axis); the M0 spike table, renderer-owner inventory with archived documentation, claim-sketch handle-resolution note, third-party-component obligation sentence, and the tightened Acto ingress-identity wording (action-root ingress landed; end-to-end realistic-event binding remains new owner work) all landed.
+
+- **rev 9** — round-8 review response (Fable READY / Codex NOT READY): the release-profile contradiction rev 8 introduced is resolved by choosing the honest construction — the profile is renamed **`release-configuration-attested`**, certifying an instrumented sibling built from the same source revision and effective configuration in release configuration, differing only by the verification channel, with shared/differing inputs attested at `measured`/`signed` class; because production stripping means the distributed bytes carry no verifier, v1 makes no claims about shipped store bytes, period — even the strongest profile authorizes only "release-configuration-verified," with exact-byte designs (dormant channel; two-artifact attestation of shared payload sections plus wrapper) deferred to §6; the rev 8 "runtime monitor is the only net" sentence corrected (the monitor checks only authored contract-block clauses; the dev dataflow inspector detects mismatches only on invocation; unvisited headerless actions produce no evidence at all) and the handler-identity sentence extended to `press`/`change`; M-1/M0 bounded — every item/spike carries a staff-week and calendar ceiling assigned in the funding brief, spikes are ordered by information value with preregistered stop/narrow/pivot outcomes, M0 becomes independently gated experiments, a named **minimum viable protocol core** (host semantics, identity, opaque regions, layout + native-reachability, attestation, conformance) is what still justifies M1 under pessimistic spike findings with Fiber provenance/claims/provenance-tainting/release certification as separately fundable layers, and post-M0 the strategy splits into separately governed specs with this RFC as the map, not the contract; a short-form M-1 pitch precedes the funding ask; the worst-case operability contingency (both input paths fail everywhere) is preregistered at M0 (re-scope completion to `semantic-development` + `userReachable` under permanently qualified language, or kill the reliability thesis); downgrade share gets an M0-set ceiling and downgrade audit gets a named approver and recorded artifact; the Acto probe scope split per platform (same-app on macOS, same-scene on iOS) with the full enumeration residual labeled new RASP behavior; the target-equivalence fingerprint anchored to Exact's shipped staleness fact list as the auditable baseline; route-scoped claims declared permanently first-class; upstream extension points preferred with private patches as a budgeted fallback; and M3 entry now requires the minimum scenario model (runner ownership/hosting resolving OQ16, settling, coverage-ledger semantics, unavailable-driver verdict effect) before claims become a product commitment.
+
+- **rev 8** — round-7 review response (Fable READY / Codex NOT READY): attestation gains 0374 W1's **evidence-strength axis** — each attested field carries `self-declared` | `compared` | `measured` | `signed` plus authority/freshness/invalidation, profiles name minimum classes, loaded-bytes and configuration claims require `measured`/`signed`, and a self-reported-manifest-while-running-different-bytes fixture must fail; the release artifact model chosen explicitly — the profile is renamed **`release-candidate-attested`**, certifying the exact release-built candidate bytes (digest-chained from the EAS build, tethered dev-only channel, pre-submission), v1 makes no claims about distributed store bytes (an instrumented-sibling conformance case proves the digest obligation cannot be satisfied by a stand-in; the dormant-channel-in-shipped-binary option is deferred to §6 with its threat/size cost named), the profile's driver leg explicitly requires verification grade with the OQ4-pessimistic contingency recorded (profile unattainable on that platform, never quietly weakened), and M4's shipped-language scoping updated; the rev 7 `writes` claim corrected (it was false, caught by Codex with code citations that match the orchestrator's own round-2 verification): the action-header `writes` list is optional — no header, no static write diagnostics; when present, exact-set checked with undeclared-write errors and unused-declaration warnings; contract-block clauses optional and exact-set-error-checked when authored; for undeclared actions the runtime monitor is the only net; M0's compatibility test rebuilt to price the moat rather than presume it (full hook inventory across public/private/upstream/patched/Expo-owned, a representative supported SDK/RN window instead of one adjacent version, an ownership/staffing/compatibility-cost model with per-milestone size bands for the decision brief, a named upstream-extension-point or maintained-patch strategy, an explicit host-semantic fallback if Fiber internals prove brittle) plus a bounded OS-driver correlation spike so release-grade attainability is learned before M2/M3 investment; `responder-operability` gains an explicit qualified-status contract (always visibly qualified where an enumeration residual exists; never renderable or exportable as an unqualified pass through Expo Agent, CI, or durable artifacts); the Exact security precedent restated accurately (borrowed: loopback defaults, registration, Host/origin validation, authenticated effects and selected sensitive reads; RASP's gating of ordinary structure/screenshot reads deliberately strengthens a posture Exact does not ship); §2.2 item 4 narrowed to accessibility/semantic-snapshot diagnosability and the benchmarked tools; content-provenance class names aligned to LLP 0271's shipped taxonomy (`app`/`system`/`other-user`/`generated`/`data-unknown`) so laundering fixtures compose; a route-scoped `routeClaims` example now leads §3.4 with the component-registered form labeled second-phase; the M2 held-out arm's scope stated (evidence protocol, independent of the M3 claims product); a cross-action substitution mutation added (operability for A + OS delivery for B cannot compose into release-candidate certification); the input-fidelity vocabulary attributed to LLP 0306/adapter shape rather than a uniform tap result; schema/wire-corpus naming made independent of the interim RASP name; a react-native-web arm noted as a candidate cheap deployment surface for the protocol half; and the evidence-report artifact (claims × verdicts × facets × residuals, attestation tuple, ledger coverage) named as the user-visible output.
+
+- **rev 7** — round-6 review response (both reviewers NOT READY; the consequence-classifier gap found independently by both): minting-vs-propagation corrected — the in-process tier's minted identity identifies the intended event only; verification-grade status requires per-platform proof that the same identity is bound to the concrete native event and observed at the action token (ambient/concurrent/dropped/diverted/delayed cases), with the non-driver `userOperable` path disabled on platforms lacking that proof, the Apple precedent stated accurately (`UIWindow.sendEvent` iOS / `NSWindow.sendEvent` macOS; Android `MotionEvent` is proposed RN work), and the tier table given LLP 0306's known-gap form; reachability facts declare a per-platform enumeration scope with out-of-scope occlusion reported as an explicit unverifiable residual (a system permission dialog can defeat both `userReachable` and in-process `userOperable`; the profiles now say so), the `userOperable` gloss qualified accordingly, and `physical-operability` renamed **`responder-operability`** with `release-attested` *requiring* OS-driver input (closing the cross-process residual) and alone entitled to shipped-app language, per platform; the consequence classifier named (provenance-bearing descriptor on the action token, Expo Agent codegen-emitted, lint-checked) with a fail-safe default — unclassified exercised interactive gestures are consequence-bearing unless explicitly downgraded — plus a coverage metric and an unlabeled-destructive-action mutation fixture; content provenance rebuilt to the LLP 0271/0374 W5 model (authorship and disclosure/redaction as separate axes; dynamic content defaults to hostile `data-unknown`; only static literals default `app-authored`; named authorities; taint/laundering fixtures before provenance gates anything); the held-out endpoint made abstention-proof (joint endpoint: false-pass at matched completion/obligation-resolution coverage, all abstention classes counted, non-inferiority floor, corpus sealed under an independent custodian); `navigatesTo` given the full causal contract (action-token observation joined at a named grade to an Expo Router transition + settled-route fact on the same attested run; ambient/timer navigation cannot pass; the Exact precedent labeled specification-only since Phase A ignores its navigation metadata) and a router-state row added to the §3.0 fact table; certification and milestone exits made per-platform (M3 = explicitly a one-platform pilot until the operability mutation suite runs on each advertised platform; M4 shipped-app claims require an end-to-end `release-attested` run per covered platform); the `writes` wording fixed (undeclared writes are a compile error; the contract-block clause is what's optional); passive invariants noted as root-mounted auto-watching; verdicts stated as attestation-bound; the manifest-drift limit acknowledged (registry drift impossible, behavioral drift beyond corpus coverage possible); a non-negotiable minimum named if Expo trims the apparatus (zero-false-pass negatives, sealed held-out gate, fail-closed attestation); and the revision history moved to this appendix (both reviewers, both rounds).
+
+- **rev 6** — round-5 review response (both reviewers NOT READY; concerns disjoint but compatible): a **target/artifact attestation fact family** added per LLP 0374 W1 — every claim and receipt binds to the attested tuple (project, artifact digest, runtime version, effective configuration, process epoch), verification fails closed on mismatch/unknown, with wrong-project/stale-bundle/different-config/mixed-HMR/process-restart/dev-vs-release fixtures, and the source-native fact graph declared authoritative with any fused tree as a non-erasing projection plus a content-provenance axis (W5's prompt-injection boundary); the ordered "proof frontier" corrected to a **directed proof graph** (semantic-wired and native-hit-reachable are independent nodes; derived tier labels carry no authority, per 0374 W3 and Exact's causal-trace rule) with claims naming required nodes/edges/grades; the input-mechanism ownership contradiction resolved — RASP includes a runtime-owned **in-process responder-path delivery tier** (the RN analog of Acto's shipped LLP 0306 P1 windowSendEvent/touch-sequence tier, ingress-propagated by construction because the synthesizer mints the identity), §3.6's non-goal reworded to partner-owned *OS/device-level* synthesis, §3.0 gains runtime-witnessed-ingress and in-process-delivery rows, and per-platform feasibility is an M0 spike item; **certification profiles** (`semantic-development` / `physical-operability` / `release-attested`) expand mechanically to required facets, Expo Agent's generalized "done" requires `physical-operability` for consequence-bearing gestures via default auto-escalated `userOperable` obligations (the broken-responder case now fails the default completion decision with no authored stronger claim), and `semantic-development` can gate only labeled development checkpoints; claim scopes sequenced — route/root-scoped claims ship before component-registered claims to decouple the React identity risk — and a `press(x).navigatesTo(route)` claim kind added (Expo Router outcome claims, the LLP 0085 navigation clause family); stale-ref precision corrected — the graded/receipted path is flag-gated in Exact and its equivalence check is event-kind-only (a handler swap keeping `onPress` can stale-verify), so RASP defines a normative **target-equivalence fingerprint** (semantic/owner identity, action-token identity, interactable state, trust/consequence posture, reachability class) with swap/remount/trust/reachability fixtures and stale refs rejecting until fingerprint facts exist, and the §3.7 rows split accordingly; the **held-out experiment is attached to a binding gate** — fully preregistered at M0 (split frozen pre-implementation, unit, primary false-pass endpoint, best-cheap-arm comparator, minimum effect, kill/pivot consequence), run at M2 exit as the M3 entry condition; M-1 incident classification gets a **mechanical RASP-addressability rule with a non-author adjudicator** (the counterfactual-bias hole in the preregistration discipline, closed); written partner confirmation becomes a prerequisite for *selecting the standardization thesis* (with an interoperability reframe if an existing renderer contract surfaces) while the reliability thesis needs no partner input; §2.2 item 4 narrowed to undiagnosable-outside-in/undetectable-semantic (the switcher-clicks symptom was visible to real input); the two Exact-baseline qualifications landed (strict-mode opaque-import errors in pure/resource contexts; tap-receipt action/owner provenance as optional Contract-path dev-mode precedent); `userReachable` probes evaluate at the exercising scenario step; M1 adds ladder-rung mis-targeting and attestation fixtures; M3 adds claim-kind distribution tracking and requires the in-process tier at entry; M0 adds a 1–2 page decision brief; open questions add attestation ownership, Fast Refresh registration survival, and claims-runner hosting
+
+- **rev 5** — round-4 review response (Fable READY / Codex NOT READY): the exclusive-input-lease hardening path to verification grade is deleted — armed brackets are **never** verification-grade, because bracketing proves uniqueness among observed ingress, not the identity of the sole observed event (both reviewers independently found the sole-event-substitution edge: driver input dropped/diverted while one ambient event arrives inside the bracket); leases remain diagnostic-only with geometry/timing consistency reported, and the substitution cases ("intended event dropped + sole ambient substitute," "delayed prior event as sole ingress") are required conformance fixtures yielding `unresolvable`; interaction evidence is now a named **proof frontier** (`semantic-wired` → `native-hit-reachable` → `responder-delivered` → `expected-handler-observed` → `resulting-commit-attributed`) so no overloaded operability boolean can overstate weaker evidence — `userReachable` is explicitly a counterfactual hit test that does not prove the responder path (per LLP 0306's semantic-mode gaps), a new `userOperable` claim kind requires real input causally joined at verification grade through native ingress to the expected action token, `osDelivered` alone is insufficient for operability, the M3 gate's certification is relabeled honestly as "semantic wiring + native reachability, verified in development," and a composition-failure mutation case (reachable + token-dispatched + broken responder path must not pass `userOperable`) joins the M3 tests; v1 reachability is scoped to point interactions (drag/pan need trajectory reachability, undesigned); receipts preserve Acto's requested/actual/servedBy input-fidelity fields; a cold-client discoverability fixture is added at M1 (LLP 0365's behavioral lesson, not just the mechanical manifest); the M-1 corpus cost is stated honestly with its outcome-independent reuse value, and the held-out confirmation names its comparator (best cheap arm) with a preregistered minimum false-pass improvement; the targeting ladder is scoped to in-session use with cross-run artifacts (replay, claims, codegen) pinned to author/claim-local handles, and Acto's order is cited as its documented preference; the v1 claims-registration loud-failure promise gets a mechanism sketch (module-scope token; HOC wrap and new-token remount fail registration); §3.1 states the three in-process representations have no supported Expo join today; the claims example is annotated claim-local; the name-keyed-API instance-identity argument is made explicit in §3.5; partner confirmation of the load-bearing landscape assertion becomes a written two-question artifact at M0; §3.7's reachability row is corrected to "shipped before and validated by Partitime"; §3.6's ownership cross-reference is tightened; and open questions add corpus governance, semantic-id lifetime, and the per-platform ingress-observability floor folded into the driver-correlation question
+
+- **rev 4** — round-3 review response (Fable READY / Codex NOT READY): verification-grade delivery disambiguated — `osDelivered` now requires ingress-propagated identity or an exact platform event key, with `armed-bracket` acceptable only under an exclusive-input lease whose ambiguity protocol makes any competing ingress `unresolvable`, and native reachability is the default requirement for *every* user-gesture claim consumed by a "done" decision, with declared, audited exceptions that can never satisfy an operability assertion; the evidence envelope realigned with LLP 0374 and Acto's shipped crosswalk — availability is `witnessed|degraded|absent` with a separate reason class (`unavailable`/`unsupported`/`unobserved`/`evicted`/`timed-out`/`redacted`/…), redaction can be present-but-degraded, and an acceptance truth table plus conformance fixtures must prove degraded/evicted/timed-out/redacted evidence cannot satisfy required verification, published as a normative protocol appendix at M0; the Exact claims precedent tightened per code — instance-scoped witness evaluation loses instance identity at the public by-name agent join (same-name multi-instance behavior is now a required RASP conformance case), compile-time checking restated as exact-set verification of *authored, optional* declarations over analyzable IR with warning-only opaque-import boundaries and the runtime monitor as defense-in-depth, and Exact's tap receipts credited as partial precedent for action tokens (action/owner provenance exists but Phase A does not consume it); two Acto transcriptions corrected — the shipped ladder is ref → testId → label → viewId → coordinates with RASP's semantic-id placement now labeled a justified adaptation, and Exact resolves `clean` refs silently (receipted `clean` is new RASP behavior); milestones rebuilt as decision experiments — M-1 labeled exploratory with preregistered taxonomy/sampling/version pins, cheaper-alternative arms (handle-discipline + best-outside-in, devtools-only instrumentation) that RASP must beat, a schema-validator/ledger arm over existing outside-in facts, and a held-out confirmation after M0 freezes definitions; M1 gains threat-model approval as an entry condition, negative security conformance tests, expected-fact manifests for positive semantic completeness, and a handle-coverage metric; M2 disagreements require adjudication against a named oracle before RASP gates anything, any RASP false pass blocks M3, and the attribution floor gets a defined descope path (attribution degrades to heuristic-informational; kill only if fixture-scoped synthesized-input attribution also fails); M4 gets separate reliability/standardization terminal scorecards with time-bounded external-adoption evidence, internal completion counting as success only under the reliability thesis; delivery narrowed to dev clients until a dedicated Expo Go compatibility/isolation gate (with a Go payload budget) passes; New-Architecture-only scope stated; the authoring-side action-token surface sketched with the weakening rate as a tracked metric; v1 claims registration fails loudly under HMR remount; the RASP rename is an M0 exit requirement with candidates named; DevTools-plumbing version skew added to the M0 seam test; §3.1 source ownership clarified (shadow tree = renderer identity + layout intent; mounted native identity joined from the native registry)
+
+- **rev 3** — round-2 review response (Fable READY / Codex NOT READY; Codex's code-level findings adjudicated as correct where the two conflicted): the evidence model is now a normative envelope separating domain outcome, evidence availability, causal join method, producer status, producer incarnation, and freshness/coordinate space — witnessed negatives are distinct from missing evidence, expected-but-unwitnessed legs are explicit obligations, and snapshot bundles declare per-source revisions with coherent/skewed/contradictory/unavailable states (contradiction is a result, not normalized away); claim kinds now declare minimum evidence facets (`dispatches` = semantic only; `userReachable` requires a native-reachability witness with unavailable ⇒ unknown, never reachable; `osDelivered` requires correlated driver evidence) and the M3 operability gate cannot be satisfied by semantic-only success, resolving rev 2's contradiction with §5; driver correlation is downgraded honestly (only ingress-propagated identity is "exact"; prepare/arm is single-use-nonce bracketing) and contributed evidence gets a trust model (contributor principals, channel binding, replay prevention, per-contributor evidence ceilings) with adversarial fixtures gating M3 consumption; identity semantics reconciled with Acto's shipped design (every node has a semantic id, refs only for addressable nodes, the ladder includes the semantic id, staleness returns receipted clean/stale-verified for verifiably-unchanged targets, and subtree-precise staleness is labeled implemented-but-flag-gated in Exact); root-scoped app handles separated from claim-local handles for reusable components, with the Exact subtree-verification precedent marked partial (the Phase A selector runner is root-scoped); the Exact claims baseline corrected against current code rather than LLP 0278's older summary — live expression-valued counts, live `change`, live guarded `when` with guard-false skip verdicts, passive invariants, and runtime action-writes monitoring all exist today — and restated as a mechanism matrix; milestones rebuilt as investment gates (M-1 baseline of current Expo Agent + outside-in tools on the same corpus including real false-success incidents; M0 chooses the governing thesis, freezes metric definitions and thresholds including zero-false-pass on deterministic negative fixtures, tests the compatibility seam across an adjacent SDK version, states kill/pivot criteria, and separates Expo-controlled engineering exits from partner review; M2 makes observation-economics report-only and adds attribution accuracy under ambient noise plus shadow-mode disagreement calibration against outside-in tools; M3 is explicitly development-scope with a tethered release-verification artifact required before any "shipped" language; M4 treats partner adoption as an outcome, not an exit); terminology switched to "OS-driver-delivered input"; redaction extended to text, input values, screenshots, scenarios, and receipt history; the capability manifest is generated from operation/evidence registries and validated against the conformance corpus; §3.7 rows corrected (why_invisible marked new RN work; subtree-precise staleness split out; claims row upgraded); the RASP acronym collision with Runtime Application Self-Protection flagged for rename; the Expo Agent internals claim moved inside the sourcing envelope; claims-registration binding named as distinct from dispatch attribution and scoped to v2; a minimum-viable-fact-set de-risking statement added; the scope note gains a handoff clause making this a dated source document once Expo decides
+
+- **rev 2** — round-1 review response: RASP recast as a provenance-bearing evidence protocol; Partitime false-positive class designed in; receipt economics restated per LLP 0326; unsupported Acto-origin history removed; LLP 0032 provenance corrected; perf-risk answer rewritten around the B1 incremental-serialization lesson; claims API made executable; M0 changed to an architecture spike; development threat model added; fixture-corpus milestones; landscape sourcing; capability provenance matrix added
+
+- **rev 1** — initial draft from the 2026-07-21 Exact→Expo lessons conversation
diff --git a/apps/router-e2e/.env b/apps/router-e2e/.env
index 0dc40644307cfa..fdbeb4104e3ad8 100644
--- a/apps/router-e2e/.env
+++ b/apps/router-e2e/.env
@@ -3,7 +3,6 @@ EXPO_NOT_PUBLIC_TEST_VALUE=not-public-value
EXPO_SKIP_MANIFEST_VALIDATION_TOKEN=0
EXPO_NO_TELEMETRY=1
TEST_SECRET_VALUE=test-secret
-# EXPO_UNSTABLE_WEB_MODAL=1
EXPO_UNSTABLE_LOG_BOX=1
EXPO_DEBUG_LOG_BOX=1
diff --git a/apps/router-e2e/__e2e__/web-modal/app/_layout.tsx b/apps/router-e2e/__e2e__/web-modal/app/_layout.tsx
deleted file mode 100644
index e4ca690ec768d5..00000000000000
--- a/apps/router-e2e/__e2e__/web-modal/app/_layout.tsx
+++ /dev/null
@@ -1,94 +0,0 @@
-import { Stack } from 'expo-router';
-
-export const unstable_settings = {
- anchor: 'index',
-};
-
-export default function Layout() {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/router-e2e/__e2e__/web-modal/app/index.tsx b/apps/router-e2e/__e2e__/web-modal/app/index.tsx
deleted file mode 100644
index 39712510b2f4d5..00000000000000
--- a/apps/router-e2e/__e2e__/web-modal/app/index.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-import { Link } from 'expo-router';
-import { View, StyleSheet } from 'react-native';
-
-export default function Page() {
- return (
-
-
-
-
- Sheet (0.5, 1)
-
-
-
- Sheet (0.25, 0.5, 0.75, 0.98)
-
-
-
- Sheet (Fit To Contents)
-
-
-
- Sheet (border radius 40)
-
-
-
- Sheet (background color red)
-
-
-
-
-
- Modal (Regular)
-
-
- Modal (Regular with red background)
-
-
- Modal (Full Screen)
-
-
-
- Modal (fit content)
-
-
-
- Modal (transparent)
-
-
- Page
-
-
-
-
- );
-}
-
-const styles = StyleSheet.create({
- button: {
- width: 180,
- padding: 12,
- borderRadius: 8,
- marginBottom: 12,
- alignItems: 'center',
- backgroundColor: '#333',
- color: 'white',
- fontSize: 14,
- },
-});
diff --git a/apps/router-e2e/__e2e__/web-modal/app/modal-fit.tsx b/apps/router-e2e/__e2e__/web-modal/app/modal-fit.tsx
deleted file mode 100644
index a8af596b94267c..00000000000000
--- a/apps/router-e2e/__e2e__/web-modal/app/modal-fit.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { Text, View } from 'react-native';
-
-export default function Page() {
- return (
-
-
- Modal that should fit content
-
- This is a small, centred modal to demonstrate default behaviour.
-
- );
-}
diff --git a/apps/router-e2e/__e2e__/web-modal/app/modal-margin.tsx b/apps/router-e2e/__e2e__/web-modal/app/modal-margin.tsx
deleted file mode 100644
index 5af7258dd8ccbe..00000000000000
--- a/apps/router-e2e/__e2e__/web-modal/app/modal-margin.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import { Text, View } from 'react-native';
-
-export default function Page() {
- return (
-
- Non modal route
- This screen is not a modal, but it is pushed to the stack from a modal.
-
- );
-}
diff --git a/apps/router-e2e/__e2e__/web-modal/app/modal-multi.tsx b/apps/router-e2e/__e2e__/web-modal/app/modal-multi.tsx
deleted file mode 100644
index f15b6435be357f..00000000000000
--- a/apps/router-e2e/__e2e__/web-modal/app/modal-multi.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import { Text, View } from 'react-native';
-
-export default function Page() {
- return (
-
-
- Multi Detent Sheet
-
- This modal provides multiple detents for extensive responsiveness testing.
-
- );
-}
diff --git a/apps/router-e2e/__e2e__/web-modal/app/modal-regular-bg.tsx b/apps/router-e2e/__e2e__/web-modal/app/modal-regular-bg.tsx
deleted file mode 100644
index 35178ea004ba5d..00000000000000
--- a/apps/router-e2e/__e2e__/web-modal/app/modal-regular-bg.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import { Link } from 'expo-router';
-import { ScrollView, StyleSheet, Text, View } from 'react-native';
-
-const styles = StyleSheet.create({
- button: {
- width: 180,
- padding: 12,
- borderRadius: 8,
- marginBottom: 12,
- alignItems: 'center',
- backgroundColor: '#333',
- color: 'white',
- fontSize: 14,
- },
-});
-
-export default function Page() {
- return (
-
-
- Regular Modal (detent 0.8)
-
-
-
-
- This modal uses a single detent at 80% of the screen height. The buttons below open other
- examples or navigate elsewhere so you can test nested navigation behaviour.
-
-
-
-
- Open Sheet (radius 40)
-
-
-
- Open Transparent Modal
-
-
-
- Navigate to root
-
-
-
- Push to non-modal route
-
-
- );
-}
diff --git a/apps/router-e2e/__e2e__/web-modal/app/modal-regular.tsx b/apps/router-e2e/__e2e__/web-modal/app/modal-regular.tsx
deleted file mode 100644
index 342d0a7c28d4c3..00000000000000
--- a/apps/router-e2e/__e2e__/web-modal/app/modal-regular.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import { Link } from 'expo-router';
-import { usePreventRemove } from 'expo-router/react-navigation';
-import { useState } from 'react';
-import { Alert, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
-
-const styles = StyleSheet.create({
- button: {
- width: 180,
- padding: 12,
- borderRadius: 8,
- marginBottom: 12,
- alignItems: 'center',
- backgroundColor: '#333',
- color: 'white',
- fontSize: 14,
- },
-});
-
-export default function Page() {
- const [isPrevented, setIsPrevented] = useState(false);
- usePreventRemove(isPrevented, () => {
- Alert.alert('Prevented!', 'You cannot dismiss this modal right now.');
- });
- return (
-
-
- Regular Modal (detent 0.4)
-
-
-
-
- This modal uses a single detent at 40% of the screen height. The buttons below open other
- examples or navigate elsewhere so you can test nested navigation behaviour.
-
-
-
-
- Open Sheet (radius 40)
-
-
-
- Open Transparent Modal
-
-
-
- Navigate to root
-
-
-
- Push to non-modal route
-
-
- {
- setIsPrevented((p) => !p);
- }}>
- {isPrevented ? 'Disable' : 'Enable'} preventRemove
-
-
- );
-}
diff --git a/apps/router-e2e/__e2e__/web-modal/app/modal-scroll.tsx b/apps/router-e2e/__e2e__/web-modal/app/modal-scroll.tsx
deleted file mode 100644
index 7d8273307cfd7e..00000000000000
--- a/apps/router-e2e/__e2e__/web-modal/app/modal-scroll.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { ScrollView, Text } from 'react-native';
-
-export default function Page() {
- return (
-
-
- Modal with scrollable content
-
- {Array.from({ length: 60 }).map((_, idx) => (
- This modal should occupy the full screen (no detents).
- ))}
-
- );
-}
diff --git a/apps/router-e2e/__e2e__/web-modal/app/modal-transparent.tsx b/apps/router-e2e/__e2e__/web-modal/app/modal-transparent.tsx
deleted file mode 100644
index 4dda67b1e5b500..00000000000000
--- a/apps/router-e2e/__e2e__/web-modal/app/modal-transparent.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import { useRouter } from 'expo-router';
-import { usePreventRemove } from 'expo-router/react-navigation';
-import { useState } from 'react';
-import { Alert, Pressable, StyleSheet, Text, View } from 'react-native';
-
-const styles = StyleSheet.create({
- button: {
- width: 180,
- padding: 12,
- borderRadius: 8,
- marginBottom: 12,
- alignItems: 'center',
- backgroundColor: '#333',
- color: 'white',
- fontSize: 14,
- },
-});
-
-export default function Page() {
- const [isPrevented, setIsPrevented] = useState(false);
- const router = useRouter();
- usePreventRemove(isPrevented, () => {
- Alert.alert('Prevented!', 'You cannot dismiss this modal right now.');
- });
- return (
-
- Transparent Modal
-
- This is a transparent modal to demonstrate default behaviour. You need to navigate or use
- the back button to dismiss it. You can't dismiss it by tapping outside the modal.
-
-
- The modal is transparent, so you can see the background behind it. The background color is
- not part of the modal, it's the background of the page.
-
- {
- router.back();
- }}>
- Go back
-
- {
- router.dismissAll();
- }}>
- Dismiss all
-
- {
- setIsPrevented((p) => !p);
- }}>
- {isPrevented ? 'Disable' : 'Enable'} preventRemove
-
-
- );
-}
diff --git a/apps/router-e2e/__e2e__/web-modal/app/modal.tsx b/apps/router-e2e/__e2e__/web-modal/app/modal.tsx
deleted file mode 100644
index 8a5405962d3c3d..00000000000000
--- a/apps/router-e2e/__e2e__/web-modal/app/modal.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { Text, View } from 'react-native';
-
-export default function Page() {
- return (
-
-
- Make Modals Great Again
-
- This modal is rendered using the custom drawer integration for web.
-
- );
-}
diff --git a/apps/router-e2e/__e2e__/web-modal/app/nested/_layout.tsx b/apps/router-e2e/__e2e__/web-modal/app/nested/_layout.tsx
deleted file mode 100644
index f5348f1bf30745..00000000000000
--- a/apps/router-e2e/__e2e__/web-modal/app/nested/_layout.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { Stack } from 'expo-router';
-
-export const unstable_settings = {
- anchor: 'page',
-};
-
-export default function Layout() {
- return (
-
-
-
-
-
- );
-}
diff --git a/apps/router-e2e/__e2e__/web-modal/app/nested/modal-transparent.tsx b/apps/router-e2e/__e2e__/web-modal/app/nested/modal-transparent.tsx
deleted file mode 100644
index f57a23f8c00c1d..00000000000000
--- a/apps/router-e2e/__e2e__/web-modal/app/nested/modal-transparent.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import { Text, View } from 'react-native';
-
-export default function Page() {
- return (
-
- Transparent Modal
- This is a transparent modal to demonstrate default behaviour.
-
- );
-}
diff --git a/apps/router-e2e/__e2e__/web-modal/app/nested/page.tsx b/apps/router-e2e/__e2e__/web-modal/app/nested/page.tsx
deleted file mode 100644
index 7be14e931165eb..00000000000000
--- a/apps/router-e2e/__e2e__/web-modal/app/nested/page.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import { Link } from 'expo-router';
-import { usePreventRemove } from 'expo-router/react-navigation';
-import { useState } from 'react';
-import { Alert, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
-
-const styles = StyleSheet.create({
- button: {
- width: 180,
- padding: 12,
- borderRadius: 8,
- marginBottom: 12,
- alignItems: 'center',
- backgroundColor: '#333',
- color: 'white',
- fontSize: 14,
- },
-});
-
-export default function Page() {
- const [isPrevented, setIsPrevented] = useState(false);
- usePreventRemove(isPrevented, () => {
- Alert.alert('Prevented!', 'You cannot dismiss this modal right now.');
- });
- return (
-
-
- Default page in nested layout
-
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut
- labore et dolore magna aliqua.
-
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut
- labore et dolore magna aliqua.
-
-
-
- Sheet
-
-
- Modal (transparent)
-
-
- {
- setIsPrevented((p) => !p);
- }}>
- {isPrevented ? 'Disable' : 'Enable'} preventRemove
-
-
- );
-}
diff --git a/apps/router-e2e/__e2e__/web-modal/app/nested/sheet-radius.tsx b/apps/router-e2e/__e2e__/web-modal/app/nested/sheet-radius.tsx
deleted file mode 100644
index e56d95797994b4..00000000000000
--- a/apps/router-e2e/__e2e__/web-modal/app/nested/sheet-radius.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import { Text, View } from 'react-native';
-
-export default function Page() {
- return (
-
- Large Radius Sheet
- This sheet demonstrates a custom large corner radius.
-
- );
-}
diff --git a/apps/router-e2e/__e2e__/web-modal/app/sheet-bg.tsx b/apps/router-e2e/__e2e__/web-modal/app/sheet-bg.tsx
deleted file mode 100644
index e834a291869d3c..00000000000000
--- a/apps/router-e2e/__e2e__/web-modal/app/sheet-bg.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { Text, View } from 'react-native';
-
-export default function Page() {
- return (
-
-
- Sheet with red background
-
- This sheet tests the background color.
-
- );
-}
diff --git a/apps/router-e2e/__e2e__/web-modal/app/sheet-fit.tsx b/apps/router-e2e/__e2e__/web-modal/app/sheet-fit.tsx
deleted file mode 100644
index bd8094d959e0af..00000000000000
--- a/apps/router-e2e/__e2e__/web-modal/app/sheet-fit.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Text, View } from 'react-native';
-
-export default function Page() {
- return (
-
-
- Fit To Contents Sheet
-
-
- This sheet height fits its contents using the custom fitToContents detent. but it's using
- its defaults on the modal. Shrinking to mobile size switches to fit to contents.
-
-
- );
-}
diff --git a/apps/router-e2e/__e2e__/web-modal/app/sheet-radius.tsx b/apps/router-e2e/__e2e__/web-modal/app/sheet-radius.tsx
deleted file mode 100644
index e56d95797994b4..00000000000000
--- a/apps/router-e2e/__e2e__/web-modal/app/sheet-radius.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import { Text, View } from 'react-native';
-
-export default function Page() {
- return (
-
- Large Radius Sheet
- This sheet demonstrates a custom large corner radius.
-
- );
-}
diff --git a/apps/router-e2e/package.json b/apps/router-e2e/package.json
index d33f9b6a248dcc..56db64a6e7a27f 100644
--- a/apps/router-e2e/package.json
+++ b/apps/router-e2e/package.json
@@ -32,8 +32,6 @@
"android:native-navigation": "E2E_ROUTER_SRC=native-navigation expo run:android",
"export:native-navigation": "EXPO_ATLAS=1 E2E_ROUTER_SRC=native-navigation expo export",
"start:headless": "E2E_ROUTER_SRC=headless expo",
- "start:web-modal": "E2E_ROUTER_SRC=web-modal EXPO_WEB_DEV_HYDRATE=1 expo start",
- "export:web-modal": "E2E_ROUTER_SRC=web-modal expo export -p web",
"start:fast-refresh": "E2E_ROUTER_SRC=fast-refresh expo start -w",
"start:middleware-async": "E2E_ROUTER_SRC=server-middleware-async EXPO_USE_STATIC=server E2E_ROUTER_SERVER_MIDDLEWARE=true expo start",
"export:middleware-async": "E2E_ROUTER_SRC=server-middleware-async EXPO_USE_STATIC=server E2E_ROUTER_SERVER_MIDDLEWARE=true expo export -p web",
diff --git a/docs/pages/router/advanced/custom-navigators.mdx b/docs/pages/router/advanced/custom-navigators.mdx
index 9f07d67288b02a..248a7f6f5572ea 100644
--- a/docs/pages/router/advanced/custom-navigators.mdx
+++ b/docs/pages/router/advanced/custom-navigators.mdx
@@ -17,6 +17,8 @@ Choose the entry point that matches your goal:
- **App developers** building a navigator for one app use [`unstable_createStandardRouterNavigator`](#create-a-navigator-in-your-app).
- **Library authors** shipping a reusable navigator for both Expo Router and React Navigation use [`unstable_integrateWithRouter`](#integrate-an-existing-navigator-library-authors).
+For a stack navigator that renders routes as web modal overlays, see [Build custom web modals](/router/advanced/web-modals/).
+
## Create a navigator in your app
Use `unstable_createStandardRouterNavigator` to turn a content component into a navigator you can render as a layout. It takes two required arguments:
diff --git a/docs/pages/router/advanced/web-modals.mdx b/docs/pages/router/advanced/web-modals.mdx
index d894242db05faa..74f633fd997de3 100644
--- a/docs/pages/router/advanced/web-modals.mdx
+++ b/docs/pages/router/advanced/web-modals.mdx
@@ -1,34 +1,21 @@
---
-title: Web modals
-description: Learn how to implement and customize the behavior of a modal in your web app using Expo Router.
+title: Build custom web modals
+description: Learn how to build modal overlays for web with Expo Router.
---
-import { Collapsible } from '~/ui/components/Collapsible';
-import { ContentSpotlight } from '~/ui/components/ContentSpotlight';
-import { FileTree } from '~/ui/components/FileTree';
+Expo Router no longer provides an experimental web modal implementation. On web, screens with `presentation: 'modal'` or `presentation: 'formSheet'` render as regular stack routes.
-> **important** Web modals are in [alpha](/more/release-statuses/#alpha) and available in SDK 54 and later. To use this feature, you must set the `EXPO_UNSTABLE_WEB_MODAL=1` environment variable in your project.
+> **warning** If you previously enabled `EXPO_UNSTABLE_WEB_MODAL`, remove it from your environment. The `webModalStyle` screen option and the `--expo-router-modal-*` CSS variables are no longer available.
-Modern web apps require a flexible modal experience that adapts to different content sizes and user interactions. Expo Router provides various modal presentation patterns for modern web experiences. These patterns leverage `presentation` with `modal`, `formSheet`, `transparentModal`, or `containedTransparentModal` to present either a modal based on different screen widths, and provide customizable styling props using `webModalStyle`.
+## Use a transparent overlay route
-## Get started
-
-> **info** To use the new web modal features, you must set the `EXPO_UNSTABLE_WEB_MODAL=1` environment variable for both development and [export](/deploy/web/#export-your-web-project) builds. You can do this by adding it to your **.env** file at the root of your project or by prefixing your commands, for example: `EXPO_UNSTABLE_WEB_MODAL=1 npx expo start`.
-
-Modals in Expo Router are configured using `Stack.Screen` component with specific options. This requires the modal screen to be added to the layout file of your app's `Stack`.
-
-Consider the following navigation tree, which includes a stack navigator defined in the layout file, a home screen where the modal is accessed, and the modal screen component:
-
-
-
-In the layout file (**src/app/\_layout.tsx**), the modal screen component is added to the Stack navigator:
+For most apps, use the built-in `Stack` and set the modal route to `transparentModal` on web. Expo Router keeps the previous route visible behind a transparent modal, so the route component only needs to render the backdrop and dialog.
```tsx src/app/_layout.tsx
import { Stack } from 'expo-router';
+import { Platform } from 'react-native';
-export const unstable_settings = {
- anchor: 'index',
-};
+export const unstable_settings = { anchor: 'index' };
export default function Layout() {
return (
@@ -37,8 +24,9 @@ export default function Layout() {
@@ -46,501 +34,151 @@ export default function Layout() {
}
```
-The **modal.tsx** is used to display the contents of a modal:
-
-```tsx src/app/modal.tsx
-import { Text, View } from 'react-native';
+Use a [platform-specific component](/router/advanced/platform-specific-modules/) for the visual treatment. The web implementation handles the backdrop, the Escape key, and dismissal. The native implementation returns `children` unchanged, so **modal.tsx** can wrap its content in `WebModal` on every platform.
-export default function Modal() {
- return {/* Modal content goes here */};
-}
-```
+```tsx src/components/WebModal.web.tsx
+import { router } from 'expo-router';
+import { useEffect, type ReactNode } from 'react';
+import { Pressable, StyleSheet, View } from 'react-native';
-Now, to open the modal from **index.tsx**, you can use `router.push('/modal')` in your index route:
+export function WebModal({ children }: { children: ReactNode }) {
+ useEffect(() => {
+ const onKeyDown = (event: KeyboardEvent) => {
+ if (event.key === 'Escape' && router.canGoBack()) router.back();
+ };
+ window.addEventListener('keydown', onKeyDown);
+ return () => window.removeEventListener('keydown', onKeyDown);
+ }, []);
-```tsx src/app/index.tsx
-import { router } from 'expo-router';
-import { Pressable, Text, View, StyleSheet } from 'react-native';
+ const dismiss = () => (router.canGoBack() ? router.back() : router.replace('/'));
-export default function Home() {
return (
-
- Home Screen
- router.push('/modal')} style={styles.button}>
- Open modal
-
+
+
+
+ {children}
+
);
}
const styles = StyleSheet.create({
- container: {
+ overlay: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
+ backgroundColor: 'rgba(0, 0, 0, 0.4)',
+ padding: 24,
},
- title: {
- fontSize: 24,
- fontWeight: 'bold',
- marginBottom: 20,
- },
- button: {
- backgroundColor: '#007AFF',
- padding: 16,
- borderRadius: 8,
- },
- buttonText: {
- color: 'white',
- fontSize: 16,
- fontWeight: '600',
- },
+ dialog: { width: '100%', maxWidth: 640, borderRadius: 16, backgroundColor: 'white', padding: 24 },
});
```
-Here's the result of the above example:
-
-
-
-## Anchors and nested stacks
-
-When working with stack or nested stack navigators, modals need to be properly anchored to ensure correct navigation behavior, especially when deep-linking to modal routes. Without anchoring, the screen behind the modal will be wiped away, leaving no navigation context.
-
-An _anchor_ serves as the base for the modal. In complex apps, when you have nested stacks, the anchor must be defined for the nested stack, and its value becomes the initial route of the stack.
-
-You can configure an anchor by exporting `unstable_settings` from your stack's layout file:
-
-```tsx
-export const unstable_settings = {
- anchor: 'index', // Anchor to the index route
-};
-```
-
-In the above example, the `anchor: 'index'` tells the Expo Router that it should maintain the specified anchor route in the background when presenting a modal.
-
-## Modal presentation style
-
-The difference between the presentation of how a modal appears in your web app on a large screen (for example, a desktop) while maintaining the sheet behavior when the web app runs on a mobile device, depends on the configuration options. The following are options available for configuring a web modal's appearance that can be passed to the `options` object of a `Stack.Screen`.
-
-| Option | Type | Description |
-| --------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `presentation` | `'modal'`, `'formSheet'`, `'transparentModal'`, `'containedTransparentModal'` | Modal presentation style. On screens with width more than 768px, all styles display as a centered overlay (for example, a lightbox).
On screens with width less than `768px`, `formSheet` is used to display as a bottom sheet.
When set to `transparentModal`, it displays as an overlay without a completely obscure background content. Detents and sheet grabber properties are not applied. This presentation is useful when building your own custom modal.
Similar to `transparentModal`, when set to `containedTransparentModal`, it displays as an overlay without a completely obscure background content. Detents and other properties are not applied. This presentation is useful when building your own custom modal. |
-| `sheetAllowedDetents` | `number[]`, `'fitToContents'` | Snap positions as percentages (0.0-1.0) or automatic fitting. Only applies to screens with less than `768px` width. |
-| `sheetGrabberVisible` | `boolean` | **On iOS, s**hows/hides the drag handle at the top of the sheet. Not supported on Android and web. We recommend using a custom sheet header component to imitate the grabber across all platforms. |
-| `sheetCornerRadius` | `number` | Corner radius of the sheet in pixels. |
-| `webModalStyle` | `WebModalStyle` | Special prop that allows web-specific styling options for fine-tuning modal appearance. |
-
-## Custom modal styling with `webModalStyle`
-
-> **info** **Note:** The `webModalStyle` properties only apply to web platforms. On mobile, the modal will automatically adapt to use sheet-like behavior for touch interaction.
-
-You can use `webModalStyle` to customize the dimensions and appearance of your modals on web. It provides the following properties for further customization:
-
-| Property | Type | Description | Default |
-| ------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------ | ---------------------- |
-| `width` | `number` `string` | Override the width of the modal (px or percentage). Only applies for web platform on a desktop. | `83vw` |
-| `height` | `number` `string` | Override the height of the modal (px or percentage). Only applies for web platform on a desktop. | `79vh` |
-| `minHeight` | `number` `string` | Minimum height of the desktop modal (px or percentage). Overrides the default iOS 26 sizing. | `min(586px, 79vh)` |
-| `minWidth` | `number` `string` | Minimum width of the desktop modal (px or percentage). Overrides the default iOS 26 sizing. | `min(936px, 83vw)` |
-| `border` | `string` | Override the border of the desktop modal (any valid CSS border value, for example, '1px solid #ccc' or 'none') | None |
-| `overlayBackground` | `string` | Override the overlay background color (any valid CSS color or rgba/hsla value). | Semi-transparent black |
-| `shadow` | `string` | Override the modal shadow filter (any valid CSS filter value, for example, 'drop-shadow(0 4px 8px rgba(0,0,0,0.1))' or 'none') | Drop-shadow filter |
-
-### Custom CSS properties
-
-Expo Router uses custom CSS properties to style modals, which you can override globally using `webModalStyle`. These variables provide fine-grained control over a modal's appearance.
-
-#### Width and height sizing variables
-
-```css
-/* Default modal width (83vw on desktop, following iOS 26 specifications) */
---expo-router-modal-width: 83vw;
-
-/* Maximum modal width (936px max, 83vw by default, following iOS 26) */
---expo-router-modal-max-width: min(936px, 83vw);
-
-/* Minimum modal width (auto by default) */
---expo-router-modal-min-width: auto;
-
-/* Default modal height (79vh, following iOS 26 specifications) */
---expo-router-modal-height: 79vh;
-
-/* Minimum modal height (586px max, 79vh by default, following iOS 26) */
---expo-router-modal-min-height: min(586px, 79vh);
-```
-
-#### Border and overlay styling variables
-
-```css
-/* Modal border (none by default) */
---expo-router-modal-border: none;
-
-/* Modal corner radius (24px by default, following iOS 26) */
---expo-router-modal-border-radius: 24px;
-
-/* Modal shadow filter (drop-shadow by default) */
---expo-router-modal-shadow: drop-shadow(0 10px 8px rgb(0 0 0 / 0.04))
- drop-shadow(0 4px 3px rgb(0 0 0 / 0.1));
-
-/* Overlay background color (25% black by default) */
---expo-router-modal-overlay-background: rgba(0, 0, 0, 0.25);
-```
-
-#### How `webModalStyle` maps to CSS variables
-
-When you use `webModalStyle` to override any of the sizing variables, Expo Router automatically sets these CSS variables to the values you provide:
-
-```tsx
-// This webModalStyle configuration
-webModalStyle: {
- width: 800,
- height: 600,
- border: '2px solid blue',
- overlayBackground: 'rgba(0, 0, 0, 0.7)',
- shadow: 'drop-shadow(0 8px 16px rgba(0,0,0,0.2))',
-}
-
-// ...automatically sets these CSS variables:
-// --expo-router-modal-width: 800px
-// --expo-router-modal-height: 600px
-// --expo-router-modal-border: 2px solid blue
-// --expo-router-modal-overlay-background: rgba(0, 0, 0, 0.7)
-// --expo-router-modal-shadow: drop-shadow(0 8px 16px rgba(0,0,0,0.2))
-```
-
-## Common examples
+```tsx src/components/WebModal.native.tsx
+import type { ReactNode } from 'react';
-
-
-To create a full screen modal for content that covers maximum space, you can use `webModalStyle` property in your modal route's `Stack.Screen` options:
-
-```tsx src/app/_layout.tsx
-import { Stack } from 'expo-router';
-
-export const unstable_settings = {
- anchor: 'index',
-};
-
-export default function Layout() {
- return (
-
-
-
-
- );
-}
-```
-
-Here's the result of the above example:
-
-
-
-When running your web app on mobile devices, you can set `sheetAllowedDetents` to `fitToContents` or a custom value if you want to avoid showing a full screen modal:
-
-```tsx src/app/_layout.tsx
-import { Stack } from 'expo-router';
-
-export const unstable_settings = {
- anchor: 'index',
-};
-
-export default function Layout() {
- return (
-
-
-
-
- );
-}
-```
-
-The modal appears as a sheet on a mobile device:
-
-
-
-
-
-
-
-For smaller interactions, you can create a compact modal that fits its content:
-
-```tsx src/app/_layout.tsx
-import { Stack } from 'expo-router';
-
-export const unstable_settings = {
- anchor: 'index',
-};
-
-export default function Layout() {
- return (
-
-
-
-
- );
+export function WebModal({ children }: { children: ReactNode }) {
+ return children;
}
```
-Here's the result of the above example:
-
-
+## Build a custom navigator
-
+For apps with several web modals, create a [custom navigator](/router/advanced/custom-navigators/) with `StackRouter`. The navigator adds a `modal` screen option, renders the stack up to the last non-modal route, then layers each modal route on top. Style the overlay to match your app.
-
+> **important** `NativeStackView` is available in SDK 58 and later.
-You can set the `presentation` option to `transparentModal` when you want to display an overlay that should maintain the visual context of the underlying screen:
-
-```tsx src/app/_layout.tsx
-import { Stack } from 'expo-router';
+```tsx src/components/ModalStack.web.tsx
+import {
+ NativeStackView,
+ type NativeStackDescriptorMap,
+ type NativeStackNavigationOptions,
+ type NativeStackViewState,
+ StackRouter,
+ unstable_createStandardRouterNavigator,
+ type NavigatorContentProps,
+} from 'expo-router';
+import type { ReactNode } from 'react';
+import { Pressable, StyleSheet, View } from 'react-native';
-export const unstable_settings = {
- anchor: 'index',
-};
+type Options = NativeStackNavigationOptions & { modal?: boolean };
-export default function Layout() {
+function YourModal({ children, onDismiss }: { children: ReactNode; onDismiss: () => void }) {
return (
-
-
-
-
+
+
+ {children}
+
);
}
-```
-
-Here's the result of the above example:
-
-
-
-
-
-
-You customize the corner radius using `sheetCornerRadius`:
-
-```tsx src/app/_layout.tsx
-import { Stack } from 'expo-router';
-
-export const unstable_settings = {
- anchor: 'index',
-};
-
-export default function Layout() {
- return (
-
-
-
-
+function ModalStackContent({ state, descriptors, actions }: NavigatorContentProps) {
+ // Filter preloaded routes.
+ const activeRoutes = state.routes.slice(0, state.index + 1);
+ const lastNonModalRouteIndex = activeRoutes.findLastIndex(
+ route => !descriptors[route.key].options.modal
);
-}
-```
-
-Here's the result of the above example:
-
-
-
-
-
-
-
-You can use `sheetAllowedDetents` to define the height at which the modal can rest:
-
-```tsx src/app/_layout.tsx
-import { Stack } from 'expo-router';
-
-export const unstable_settings = {
- anchor: 'index',
-};
+ const hasModals = lastNonModalRouteIndex < state.index;
+
+ // Standard navigation descriptors use the same runtime shape as NativeStackView descriptors.
+ const nativeStackDescriptors = descriptors as unknown as NativeStackDescriptorMap;
+
+ if (!hasModals) {
+ return (
+
+ );
+ }
+
+ const baseStackRoutes = activeRoutes.slice(0, lastNonModalRouteIndex + 1);
+ const baseStackPreloadedRoutes = state.routes.slice(state.index + 1);
+ const baseStackState = {
+ ...state,
+ index: lastNonModalRouteIndex,
+ routes: [...baseStackRoutes, ...baseStackPreloadedRoutes],
+ } as NativeStackViewState;
+ const modalRoutes = activeRoutes.slice(lastNonModalRouteIndex + 1);
-export default function Layout() {
return (
-
-
-
-
+
+
+ {modalRoutes.map(route => (
+
+ {descriptors[route.key].render()}
+
+ ))}
+
);
}
-```
-Here's the result of the above example:
+export const ModalStack = unstable_createStandardRouterNavigator(ModalStackContent, StackRouter);
-
-
-
-
-## Global CSS customization
-
-For your web app, if you are using a [global CSS](https://docs.expo.dev/versions/latest/config/metro/#global-css) file in your project, you can also override width, height, border, and overlay variables.
-
-You can add custom values using the `--expo-router-*` variables in your global CSS file:
-
-```css
-/* Override default modal styling globally */
-:root {
- --expo-router-modal-width: 700px;
- --expo-router-modal-min-width: auto;
- --expo-router-modal-max-width: 95vw;
- --expo-router-modal-height: 640px;
- --expo-router-modal-min-height: 640px;
- --expo-router-modal-border: none;
- --expo-router-modal-border-radius: 16px;
- --expo-router-modal-shadow: drop-shadow(0 8px 16px rgba(0, 0, 0, 0.2));
- --expo-router-modal-overlay-background: rgba(0, 0, 0, 0.5);
-}
+const styles = StyleSheet.create({
+ overlay: {
+ ...StyleSheet.absoluteFillObject,
+ alignItems: 'center',
+ justifyContent: 'center',
+ backgroundColor: 'rgba(0, 0, 0, 0.4)',
+ padding: 24,
+ },
+ dialog: { width: '100%', maxWidth: 640, backgroundColor: 'white', padding: 24 },
+});
```
-## Custom modal route implementation
-
-{/* TODO (@aman): When publishing this guide, remove "Web modals implementation" from the modals.tsx file. */}
-
-
+```tsx src/app/_layout.web.tsx
+import { ModalStack } from '../components/ModalStack.web';
-The video above demonstrates a modal window that appears over the main content of the web page. The background dims to draw focus to the modal, which contains information for the user. This is typical behavior for web modals, where users can interact with the modal or close it to return to the main page.
-
-You can achieve the above web modal behavior by using the [`transparentModal`](https://reactnavigation.org/docs/stack-navigator/#transparent-modals) presentation mode, styling the overlay and modal content, and utilizing [`react-native-reanimated`](/versions/latest/sdk/reanimated/#installation) to animate the modal's presentation.
-
-Modify your project's root layout (**src/app/\_layout.tsx**) to add an `options` object to the modal route:
-
-```tsx src/app/_layout.tsx
-import { Stack } from 'expo-router';
-
-/* @info `unstable_settings` can be set in any stack's `_layout.tsx` file. It is used to define the initial route name for the stack, which ensures that users have a consistent starting point, especially when deep linking. */
-export const unstable_settings = {
- initialRouteName: 'index',
-};
-/* @end */
+export const unstable_settings = { anchor: 'index' };
export default function Layout() {
return (
-
-
-
-
- );
-}
-```
-
-> **info** **Note:** `unstable_settings` currently works only with `Stack` navigators.
-
-The above example sets the `index` screen as the [`initialRouteName`](/router/advanced/router-settings/#initialroutename) using [`unstable_settings`](/router/advanced/router-settings). This ensures that the transparent modal is always rendered on top of the current screen, even when users navigate to the modal screen via a direct link.
-
-Style the overlay and modal content in **modal.tsx** as shown below:
-
-{/* prettier-ignore */}
-```tsx src/app/modal.tsx|collapseHeight=250
-import { Link } from 'expo-router';
-import { Pressable, StyleSheet, Text } from 'react-native';
-import Animated, { FadeIn, SlideInDown } from 'react-native-reanimated';
-
-export default function Modal() {
- return (
-
- {/* Dismiss modal when pressing outside */}
-
-
-
-
- Modal Screen
-
- ← Go back
-
-
-
+
+
+
+
);
}
```
-
-You can customize the modal's appearance as per your needs.
diff --git a/docs/pages/versions/unversioned/sdk/haptics.mdx b/docs/pages/versions/unversioned/sdk/haptics.mdx
index 5dc1003fa2b527..8f531a1990824c 100644
--- a/docs/pages/versions/unversioned/sdk/haptics.mdx
+++ b/docs/pages/versions/unversioned/sdk/haptics.mdx
@@ -129,6 +129,9 @@ const styles = StyleSheet.create({
justifyContent: 'center',
paddingHorizontal: 16,
},
+ text: {
+ textAlign: 'center',
+ },
buttonContainer: {
flexDirection: 'row',
alignItems: 'stretch',
diff --git a/docs/pages/versions/unversioned/sdk/sharing.mdx b/docs/pages/versions/unversioned/sdk/sharing.mdx
index 26422926216cac..78d3ceeafb8208 100644
--- a/docs/pages/versions/unversioned/sdk/sharing.mdx
+++ b/docs/pages/versions/unversioned/sdk/sharing.mdx
@@ -141,6 +141,7 @@ If you are using [React Navigation](https://reactnavigation.org/), you can use t
import * as Linking from 'expo-linking';
import { createStaticNavigation } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
+import HandleShare from './HandleShare';
const RootStack = createNativeStackNavigator({
screens: {
diff --git a/docs/pages/versions/unversioned/sdk/sqlite.mdx b/docs/pages/versions/unversioned/sdk/sqlite.mdx
index d08e02df853d9d..cbabe4541df2a7 100644
--- a/docs/pages/versions/unversioned/sdk/sqlite.mdx
+++ b/docs/pages/versions/unversioned/sdk/sqlite.mdx
@@ -357,7 +357,7 @@ import { View, Text, StyleSheet } from 'react-native';
export default function App() {
return (
- }>
+ Loading...}>
diff --git a/docs/pages/versions/unversioned/sdk/ui/index.mdx b/docs/pages/versions/unversioned/sdk/ui/index.mdx
index a5bebb40b03446..8b8e3497bdd6ec 100644
--- a/docs/pages/versions/unversioned/sdk/ui/index.mdx
+++ b/docs/pages/versions/unversioned/sdk/ui/index.mdx
@@ -9,6 +9,7 @@ platforms: ['android', 'ios', 'tvos', 'expo-go']
import JetpackComposeCards from '~/scenes/versions/unversioned/ui/jetpack-compose.mdx';
import SwiftUICards from '~/scenes/versions/unversioned/ui/swift-ui.mdx';
+import UniversalCards from '~/scenes/versions/unversioned/ui/universal.mdx';
`@expo/ui` is a set of native input components that allows you to build fully native interfaces with Jetpack Compose and SwiftUI. It aims to provide the commonly used features and components that a typical app will need.
@@ -49,23 +50,4 @@ See **[Drop-in replacements](drop-in-replacements)** for API-compatible replacem
### Universal
-| Component | Description |
-| -------------------------------------- | --------------------------------------------------------------------------------------------------------- |
-| [`BottomSheet`](universal/bottomsheet) | A modal sheet that slides up from the bottom of the screen. |
-| [`Button`](universal/button) | A pressable button with multiple visual variants. |
-| [`Checkbox`](universal/checkbox) | A toggle control that represents a checked or unchecked state. |
-| [`Collapsible`](universal/collapsible) | A labelled tappable header that toggles visibility of its content. |
-| [`Column`](universal/column) | A vertical layout container for universal @expo/ui components. |
-| [`FieldGroup`](universal/fieldgroup) | A scrollable container of grouped settings-style rows. |
-| [`Host`](universal/host) | A cross-platform Host component that wraps universal @expo/ui content. |
-| [`Icon`](universal/icon) | A platform-native icon — SF Symbol on iOS, Material Symbol on Android. |
-| [`List`](universal/list) | A virtualized vertical container of rows, paired with a tappable ListItem primitive. |
-| [`Picker`](universal/picker) | A single-selection input with menu and wheel appearances. |
-| [`RNHostView`](universal/rnhostview) | A cross-platform component for hosting React Native views inside @expo/ui views. |
-| [`Row`](universal/row) | A horizontal layout container for universal @expo/ui components. |
-| [`ScrollView`](universal/scrollview) | A scrollable container that supports vertical or horizontal scrolling. |
-| [`Slider`](universal/slider) | A control for selecting a value from a continuous or stepped range. |
-| [`Spacer`](universal/spacer) | A layout spacer that produces empty space between siblings. |
-| [`Switch`](universal/switch) | A toggle control that switches between on and off states. |
-| [`Text`](universal/text) | A component for displaying styled text content. |
-| [`TextInput`](universal/textinput) | A text input backed by native SwiftUI and Jetpack Compose components, with a React Native-compatible API. |
+
diff --git a/docs/pages/versions/unversioned/sdk/ui/jetpack-compose/modifiers.mdx b/docs/pages/versions/unversioned/sdk/ui/jetpack-compose/modifiers.mdx
index 396bda6e2b1e58..ca6b6897513df5 100644
--- a/docs/pages/versions/unversioned/sdk/ui/jetpack-compose/modifiers.mdx
+++ b/docs/pages/versions/unversioned/sdk/ui/jetpack-compose/modifiers.mdx
@@ -530,6 +530,7 @@ Makes the component respond to both short-tap and long-press gestures. Wraps Com
| `options.indication` | `boolean` | Whether to show a ripple indication. Defaults to `true`. |
```tsx
+import { Text } from '@expo/ui/jetpack-compose';
import { combinedClickable } from '@expo/ui/jetpack-compose/modifiers';
**info** On iOS, to show a bottom sheet on top of another, nest the second `BottomSheet` inside the first sheet's content rather than beside it. This is a limitation of the underlying SwiftUI [`sheet`]() modifier. See [how to present multiple sheets](https://www.hackingwithswift.com/quick-start/swiftui/how-to-present-multiple-sheets) for more information.
+
+
+
+
## Installation
@@ -21,53 +38,93 @@ A modal sheet that slides up from the bottom of the screen. The sheet's visibili
### Basic bottom sheet
-```tsx BottomSheetExample.tsx
+
+
+```tsx
import { useState } from 'react';
+import { useColorScheme } from 'react-native';
import { Host, Column, Button, BottomSheet, Text } from '@expo/ui';
export default function BottomSheetExample() {
const [isPresented, setIsPresented] = useState(false);
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
return (
-
-
+ >
);
}
```
+
+
### Hiding the drag indicator
Pass [`showDragIndicator={false}`](#showdragindicator) for sheets without a handle.
-```tsx BottomSheetNoIndicatorExample.tsx
+
+
+```tsx
import { useState } from 'react';
+import { useColorScheme } from 'react-native';
import { Host, Button, BottomSheet, Text } from '@expo/ui';
export default function BottomSheetNoIndicatorExample() {
const [isPresented, setIsPresented] = useState(false);
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
return (
-
- setIsPresented(true)} />
+ <>
+
+ setIsPresented(true)} />
+ setIsPresented(false)}
showDragIndicator={false}>
- No drag handle.
+ No drag handle.
-
+ >
);
}
```
+
+
### Content padding
The sheet insets its content by default. Pass [`contentPadding`](#contentpadding) to change that inset — `0` lets a row, image, or divider reach the sheet's edges.
@@ -80,20 +137,22 @@ export default function BottomSheetContentPaddingExample() {
const [isPresented, setIsPresented] = useState(false);
return (
-
- setIsPresented(true)} />
+ <>
+
+ setIsPresented(true)} />
+ setIsPresented(false)}
contentPadding={0}>
- This banner reaches the sheet's edge.
+ This banner reaches the sheet's edge. setIsPresented(false)} />
-
+ >
);
}
```
@@ -104,51 +163,88 @@ Pass [`snapPoints`](#snappoints) to let the user drag the sheet between multiple
When sheet content can be taller than the smallest snap point, wrap it in a `ScrollView` so the overflow scrolls correctly.
-```tsx BottomSheetSnapPointsExample.tsx
+
+
+```tsx
import { useState } from 'react';
+import { useColorScheme } from 'react-native';
import { Host, BottomSheet, Button, Column, ScrollView, Text } from '@expo/ui';
export default function BottomSheetSnapPointsExample() {
const [isPresented, setIsPresented] = useState(false);
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
return (
-
- setIsPresented(true)} />
+ <>
+
+ setIsPresented(true)} />
+ setIsPresented(false)}
snapPoints={['half', 'full']}>
- Half / full sheet
- Drag the sheet between half and full screen height.
+ Half / full sheet
+ Drag the sheet between half and full screen height.
-
+ >
);
}
```
+
+
> On Android, `{ fraction }` and `{ height }` snap to the nearest of `'half'` / `'full'` — the underlying `ModalBottomSheet` only supports two resting states. The partial state is only visible when content is tall enough to exceed Material's partial threshold; give the content an explicit height or fill the available space if you need the half state on short content.
### Scrollable React Native content
The bottom sheet supports a React Native list such as `FlatList` (or a high-performance list like [FlashList](https://shopify.github.io/flash-list/) or [Legend List](https://github.com/LegendApp/legend-list)) as a child when wrapped in [`RNHostView`](rnhostview). [`snapPoints`](#snappoints) sizes the sheet, and the list scrolls within that height. With `nestedScrollEnabled`, the list scrolls its own content first; once it reaches the top edge, the remaining drag moves the sheet.
-```tsx BottomSheetScrollableExample.tsx
+
+
+```tsx
import { useState } from 'react';
-import { FlatList, Text } from 'react-native';
+import { FlatList, Text, useColorScheme } from 'react-native';
import { Host, BottomSheet, Button, RNHostView } from '@expo/ui';
const DATA = Array.from({ length: 50 }, (_, i) => `Item ${i + 1}`);
export default function BottomSheetScrollableExample() {
const [isPresented, setIsPresented] = useState(false);
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
return (
-
- setIsPresented(true)} />
+ <>
+
+ setIsPresented(true)} />
+ setIsPresented(false)}
@@ -159,15 +255,17 @@ export default function BottomSheetScrollableExample() {
style={{ flex: 1 }}
data={DATA}
keyExtractor={item => item}
- renderItem={({ item }) => {item}}
+ renderItem={({ item }) => {item}}
/>
-
+ >
);
}
```
+
+
## API
```tsx
@@ -175,3 +273,5 @@ import { BottomSheet } from '@expo/ui';
```
+
+
diff --git a/docs/pages/versions/unversioned/sdk/ui/universal/button.mdx b/docs/pages/versions/unversioned/sdk/ui/universal/button.mdx
index 93c2ddb52b8428..46b4fd517ebc12 100644
--- a/docs/pages/versions/unversioned/sdk/ui/universal/button.mdx
+++ b/docs/pages/versions/unversioned/sdk/ui/universal/button.mdx
@@ -8,9 +8,26 @@ platforms: ['android', 'ios', 'web', 'expo-go']
import APISection from '~/components/plugins/APISection';
import { APIInstallSection } from '~/components/plugins/InstallSection';
+import { ComponentExample } from '~/ui/components/ComponentExample';
+import { PlatformSpotlight, PlatformTabsGroup } from '~/ui/components/PlatformTabs';
A pressable button that renders consistently across Android, iOS, and web. Supports `filled`, `outlined`, and `text` visual variants.
+
+
+
+
## Installation
@@ -19,7 +36,20 @@ A pressable button that renders consistently across Android, iOS, and web. Suppo
### Basic button
-```tsx BasicButtonExample.tsx
+
+
+```tsx
import { Host, Button } from '@expo/ui';
export default function BasicButtonExample() {
@@ -31,11 +61,26 @@ export default function BasicButtonExample() {
}
```
+
+
### Variants
Pick a visual variant with the [`variant`](#variant) prop.
-```tsx ButtonVariantsExample.tsx
+
+
+```tsx
import { Host, Column, Button } from '@expo/ui';
export default function ButtonVariantsExample() {
@@ -51,11 +96,26 @@ export default function ButtonVariantsExample() {
}
```
+
+
### Custom content
Pass [`children`](#children) for fully custom button contents. The [`label`](#label) prop is ignored when `children` is provided.
-```tsx CustomButtonExample.tsx
+
+
+```tsx
import { Host, Button, Row, Icon, Text } from '@expo/ui';
export default function CustomButtonExample() {
@@ -79,9 +139,24 @@ export default function CustomButtonExample() {
}
```
+
+
### Disabled
-```tsx DisabledButtonExample.tsx
+
+
+```tsx
import { Host, Button } from '@expo/ui';
export default function DisabledButtonExample() {
@@ -93,6 +168,8 @@ export default function DisabledButtonExample() {
}
```
+
+
## API
```tsx
@@ -100,3 +177,5 @@ import { Button } from '@expo/ui';
```
+
+
diff --git a/docs/pages/versions/unversioned/sdk/ui/universal/checkbox.mdx b/docs/pages/versions/unversioned/sdk/ui/universal/checkbox.mdx
index adbbfe305adbde..740542af82aa37 100644
--- a/docs/pages/versions/unversioned/sdk/ui/universal/checkbox.mdx
+++ b/docs/pages/versions/unversioned/sdk/ui/universal/checkbox.mdx
@@ -8,9 +8,26 @@ platforms: ['android', 'ios', 'web', 'expo-go']
import APISection from '~/components/plugins/APISection';
import { APIInstallSection } from '~/components/plugins/InstallSection';
+import { ComponentExample } from '~/ui/components/ComponentExample';
+import { PlatformSpotlight, PlatformTabsGroup } from '~/ui/components/PlatformTabs';
A controlled checkbox. Pair [`value`](#value) with [`onValueChange`](#onvaluechange) to manage state from React.
+
+
+
+
## Installation
@@ -19,7 +36,20 @@ A controlled checkbox. Pair [`value`](#value) with [`onValueChange`](#onvaluecha
### Basic checkbox
-```tsx CheckboxExample.tsx
+
+
+```tsx
import { useState } from 'react';
import { Host, Checkbox } from '@expo/ui';
@@ -34,9 +64,24 @@ export default function CheckboxExample() {
}
```
+
+
### Disabled
-```tsx DisabledCheckboxExample.tsx
+
+
+```tsx
import { Host, Checkbox } from '@expo/ui';
export default function DisabledCheckboxExample() {
@@ -48,6 +93,8 @@ export default function DisabledCheckboxExample() {
}
```
+
+
## API
```tsx
@@ -55,3 +102,5 @@ import { Checkbox } from '@expo/ui';
```
+
+
diff --git a/docs/pages/versions/unversioned/sdk/ui/universal/collapsible.mdx b/docs/pages/versions/unversioned/sdk/ui/universal/collapsible.mdx
index d0ed6403402f80..4cc31e2f2d149a 100644
--- a/docs/pages/versions/unversioned/sdk/ui/universal/collapsible.mdx
+++ b/docs/pages/versions/unversioned/sdk/ui/universal/collapsible.mdx
@@ -8,9 +8,26 @@ platforms: ['android', 'ios', 'web', 'expo-go']
import APISection from '~/components/plugins/APISection';
import { APIInstallSection } from '~/components/plugins/InstallSection';
+import { ComponentExample } from '~/ui/components/ComponentExample';
+import { PlatformSpotlight, PlatformTabsGroup } from '~/ui/components/PlatformTabs';
`Collapsible` is a primitive that shows or hides its content with a tap on a labelled header. Controlled via [`isOpen`](#isopen) and [`onOpenChange`](#onopenchange) — each `Collapsible` manages independent state.
+
+
+
+
## Installation
@@ -19,18 +36,34 @@ import { APIInstallSection } from '~/components/plugins/InstallSection';
### Basic collapsible
-```tsx CollapsibleExample.tsx
+
+
+```tsx
import { useState } from 'react';
+import { useColorScheme } from 'react-native';
import { Host, Column, Collapsible, Text } from '@expo/ui';
export default function CollapsibleExample() {
const [open, setOpen] = useState(false);
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
return (
-
+
-
+
A primitive that toggles visibility of its content via a labelled tappable header.
@@ -40,39 +73,57 @@ export default function CollapsibleExample() {
}
```
+
+
### Accordion (one section open at a time)
Wire each `Collapsible`'s `isOpen` to a shared parent value. The component doesn't enforce exclusivity — composition is up to the consumer.
-```tsx CollapsibleAccordionExample.tsx
+
+
+```tsx
import { useState } from 'react';
+import { useColorScheme } from 'react-native';
import { Host, Column, Collapsible, Text } from '@expo/ui';
type Section = 'a' | 'b' | 'c' | null;
export default function CollapsibleAccordionExample() {
const [openSection, setOpenSection] = useState('a');
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
return (
-
+ setOpenSection(open ? 'a' : null)}
label="Section A">
- Opening B or C closes this one.
+ Opening B or C closes this one. setOpenSection(open ? 'b' : null)}
label="Section B">
- Opening A or C closes this one.
+ Opening A or C closes this one. setOpenSection(open ? 'c' : null)}
label="Section C">
- Opening A or B closes this one.
+ Opening A or B closes this one.
@@ -80,6 +131,8 @@ export default function CollapsibleAccordionExample() {
}
```
+
+
## API
```tsx
@@ -87,3 +140,5 @@ import { Collapsible } from '@expo/ui';
```
+
+
diff --git a/docs/pages/versions/unversioned/sdk/ui/universal/column.mdx b/docs/pages/versions/unversioned/sdk/ui/universal/column.mdx
index 04497247a21227..97359309437caf 100644
--- a/docs/pages/versions/unversioned/sdk/ui/universal/column.mdx
+++ b/docs/pages/versions/unversioned/sdk/ui/universal/column.mdx
@@ -8,9 +8,26 @@ platforms: ['android', 'ios', 'web', 'expo-go']
import APISection from '~/components/plugins/APISection';
import { APIInstallSection } from '~/components/plugins/InstallSection';
+import { ComponentExample } from '~/ui/components/ComponentExample';
+import { PlatformSpotlight, PlatformTabsGroup } from '~/ui/components/PlatformTabs';
A vertical layout container that arranges its children from top to bottom. Delegates to SwiftUI's [`VStack`](../swift-ui/vstack) on iOS, Jetpack Compose's [`Column`](../jetpack-compose/column) on Android, and a flex `View` on web.
+
+
+
+
## Installation
@@ -19,41 +36,79 @@ A vertical layout container that arranges its children from top to bottom. Deleg
### Basic column
-```tsx ColumnExample.tsx
+
+
+```tsx
+import { useColorScheme } from 'react-native';
import { Host, Column, Text } from '@expo/ui';
export default function ColumnExample() {
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
+
return (
- First
- Second
- Third
+ First
+ Second
+ Third
);
}
```
+
+
### Alignment
Use [`alignment`](#alignment) to position children along the cross (horizontal) axis.
-```tsx ColumnAlignmentExample.tsx
+
+
+```tsx
+import { useColorScheme } from 'react-native';
import { Host, Column, Text } from '@expo/ui';
export default function ColumnAlignmentExample() {
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
+
return (
-
+
- Centered
- Centered
+ Centered
+ Centered
);
}
```
+
+
## API
```tsx
@@ -61,3 +116,5 @@ import { Column } from '@expo/ui';
```
+
+
diff --git a/docs/pages/versions/unversioned/sdk/ui/universal/fieldgroup.mdx b/docs/pages/versions/unversioned/sdk/ui/universal/fieldgroup.mdx
index 74b712c9dddd85..0ded8fa0dd7c3b 100644
--- a/docs/pages/versions/unversioned/sdk/ui/universal/fieldgroup.mdx
+++ b/docs/pages/versions/unversioned/sdk/ui/universal/fieldgroup.mdx
@@ -8,11 +8,28 @@ platforms: ['android', 'ios', 'web', 'expo-go']
import APISection from '~/components/plugins/APISection';
import { APIInstallSection } from '~/components/plugins/InstallSection';
+import { ComponentExample } from '~/ui/components/ComponentExample';
+import { PlatformSpotlight, PlatformTabsGroup } from '~/ui/components/PlatformTabs';
A scrollable container for grouped settings-style rows, mirroring the look of an iOS Settings screen. Compose `FieldGroup.Section` (for explicit groups), `FieldGroup.SectionHeader`, and `FieldGroup.SectionFooter` slots inside.
> **Note:** `FieldGroup` scrolls and has no height of its own. It stretches to fill its parent, so give it a parent with a definite size, such as a `` or a host with an explicit height. It will not appear inside a size-to-fit container like ``, because there is no bounded height for the group to fill.
+
+
+
+
## Installation
@@ -21,13 +38,29 @@ A scrollable container for grouped settings-style rows, mirroring the look of an
### Sectioned form
-```tsx FieldGroupExample.tsx
+
+
+```tsx
import { useState } from 'react';
+import { useColorScheme } from 'react-native';
import { Host, FieldGroup, Switch, Text } from '@expo/ui';
export default function FieldGroupExample() {
const [notifications, setNotifications] = useState(true);
const [analytics, setAnalytics] = useState(false);
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
return (
@@ -38,7 +71,7 @@ export default function FieldGroupExample() {
- Version 1.0.0
+ Version 1.0.0
@@ -46,23 +79,41 @@ export default function FieldGroupExample() {
}
```
+
+
### Custom section header and footer
Use `FieldGroup.SectionHeader` and `FieldGroup.SectionFooter` to render fully styled header/footer slots in place of the default `title` text.
-```tsx FieldGroupSlotsExample.tsx
+
+
+```tsx
import { useState } from 'react';
+import { useColorScheme } from 'react-native';
import { Host, FieldGroup, Switch, Text } from '@expo/ui';
export default function FieldGroupSlotsExample() {
const [enabled, setEnabled] = useState(false);
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
return (
- Privacy
+ Privacy
@@ -79,6 +130,8 @@ export default function FieldGroupSlotsExample() {
}
```
+
+
## API
```tsx
@@ -86,3 +139,5 @@ import { FieldGroup } from '@expo/ui';
```
+
+
diff --git a/docs/pages/versions/unversioned/sdk/ui/universal/host.mdx b/docs/pages/versions/unversioned/sdk/ui/universal/host.mdx
index efc6369d7198d5..533f34cf6d98fb 100644
--- a/docs/pages/versions/unversioned/sdk/ui/universal/host.mdx
+++ b/docs/pages/versions/unversioned/sdk/ui/universal/host.mdx
@@ -8,9 +8,26 @@ platforms: ['android', 'ios', 'web', 'expo-go']
import APISection from '~/components/plugins/APISection';
import { APIInstallSection } from '~/components/plugins/InstallSection';
+import { ComponentExample } from '~/ui/components/ComponentExample';
+import { PlatformSpotlight, PlatformTabsGroup } from '~/ui/components/PlatformTabs';
A container for universal `@expo/ui` content. On Android and iOS it re-exports the platform-native [`Host` for Jetpack Compose](../jetpack-compose/host)/[`Host` for SwiftUI](../swift-ui/host), so Jetpack Compose/SwiftUI children render exactly as they would in the platform-specific packages. On web, it falls back to a React Native [`View`](https://reactnative.dev/docs/view). Use `Host` as the root of any universal subtree so the same component tree works across all three platforms.
+
+
+
+
## Installation
@@ -19,14 +36,32 @@ A container for universal `@expo/ui` content. On Android and iOS it re-exports t
### Basic usage
-```tsx HostExample.tsx
+
+
+```tsx
+import { useColorScheme } from 'react-native';
import { Host, Column, Text, Button } from '@expo/ui';
export default function HostExample() {
+ const colorScheme = useColorScheme();
+
return (
- Hello, world!
+
+ Hello, world!
+ alert('Pressed')} />
@@ -34,13 +69,28 @@ export default function HostExample() {
}
```
+
+
### Match contents sizing
Use `matchContents` to let `Host` size itself to fit its content. On Android and iOS, this is forwarded to the platform-native `Host` (see [Jetpack Compose](../jetpack-compose/host)/[SwiftUI](../swift-ui/host) for the exact platform semantics). On web, it applies `alignSelf: 'flex-start'` to the underlying `View` so the host shrinks to fit its children instead of being stretched by its parent.
> **Note:** On web, the per-axis form (`{ horizontal: true }` / `{ vertical: true }`) behaves the same as the boolean form, since `alignSelf` only controls stretching on the parent's cross axis. Components that rely on independent per-axis sizing should expect the same shrink-to-fit behavior on web regardless of which axis is opted in.
-```tsx MatchContentsExample.tsx
+
+
+```tsx
import { Host, Button } from '@expo/ui';
export default function MatchContentsExample() {
@@ -52,100 +102,215 @@ export default function MatchContentsExample() {
}
```
+
+
### Layout direction
Use `layoutDirection` to render the subtree as left-to-right or right-to-left. On Android and iOS, this is forwarded to the platform-native `Host` (see [Jetpack Compose](../jetpack-compose/host)/[SwiftUI](../swift-ui/host) for the exact platform semantics). On web, it sets the `dir` attribute on the underlying `View` so descendants inherit the chosen direction.
-```tsx LayoutDirectionExample.tsx
+
+
+```tsx
+import { useColorScheme } from 'react-native';
import { Host, Row, Text } from '@expo/ui';
export default function LayoutDirectionExample() {
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
+
return (
-
+
- First
- Second
+ First
+ Second
);
}
```
+
+
### Reacting to content layout
Use `onLayoutContent` to be notified of the current dimensions of the host's content. On Android and iOS, this is forwarded to the platform-native `Host` (see [Jetpack Compose](../jetpack-compose/host)/[SwiftUI](../swift-ui/host) for the exact platform semantics). On web, it is derived from the underlying `View`'s `onLayout` callback.
-```tsx OnLayoutContentExample.tsx
+
+
+```tsx
+import { useColorScheme } from 'react-native';
import { Host, Text } from '@expo/ui';
export default function OnLayoutContentExample() {
+ const colorScheme = useColorScheme();
+
return (
console.log(`content size: ${width}x${height}`)
}>
- Hello, world!
+
+ Hello, world!
+
);
}
```
+
+
### Filling the viewport
Use `useViewportSizeMeasurement` for content that should size to the available viewport space. On Android and iOS, this is forwarded to the platform-native `Host` (see [Jetpack Compose](../jetpack-compose/host)/[SwiftUI](../swift-ui/host) for the exact platform semantics). On web, the host's underlying `View` is given the current window's width and height; any explicit `style` you pass still wins.
-```tsx UseViewportSizeMeasurementExample.tsx
+
+
+```tsx
+import { useColorScheme } from 'react-native';
import { Host, Column, Text } from '@expo/ui';
export default function UseViewportSizeMeasurementExample() {
+ const colorScheme = useColorScheme();
+
return (
- Fills the viewport
+
+ Fills the viewport
+
);
}
```
+
+
### Ignoring safe areas
By default, `Host` respects the device safe area insets (notch, home indicator, and so on). Use `ignoreSafeArea="all"` to let content extend edge-to-edge, or `ignoreSafeArea="keyboard"` to keep safe-area padding but ignore the keyboard inset. On Android and iOS, this is forwarded to the platform-native `Host` (see [Jetpack Compose](../jetpack-compose/host)/[SwiftUI](../swift-ui/host) for the exact platform semantics). On web, it is implemented via the CSS `env(safe-area-inset-*)` values applied as padding on the underlying `View`; the default also folds in `env(keyboard-inset-*)` for pages that opt in to the [VirtualKeyboard API](https://developer.mozilla.org/en-US/docs/Web/API/VirtualKeyboard_API).
-```tsx IgnoreSafeAreaExample.tsx
-import { Host, Text } from '@expo/ui';
+
+
+```tsx
+import { useColorScheme } from 'react-native';
+import { Host, Column, Spacer, Text } from '@expo/ui';
export default function IgnoreSafeAreaExample() {
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
+
return (
-
- Extends behind the notch and home indicator
+
+
+ Behind the status bar
+
+ Behind the home indicator
+
);
}
```
+
+
### Forcing a color scheme
Use `colorScheme` to override the appearance of the subtree. Pass `'light'` or `'dark'` to force one, or omit it to follow the device setting. On Android and iOS, this is forwarded to the platform-native `Host` (see [Jetpack Compose](../jetpack-compose/host)/[SwiftUI](../swift-ui/host) for the exact platform semantics). On web, it sets `data-theme` on the underlying `View` so the design-token CSS variables resolve to the forced scheme regardless of `prefers-color-scheme`.
-```tsx HostColorSchemeExample.tsx
+
+
+```tsx
import { Host, Button } from '@expo/ui';
export default function HostColorSchemeExample() {
return (
-
+ {}} />
);
}
```
+
+
### Seeding the color theme
Use `seedColor` to derive the theme applied to the subtree from a single base color. Each platform interprets it natively. On Android, it generates a full Material 3 palette (`SchemeTonalSpot`, the same algorithm as Material You) that themes Compose children and is exposed to descendants via [`useMaterialColors`](../jetpack-compose/colors#usematerialcolorsoptions). On iOS, it is applied as the SwiftUI tint, propagating through the environment to theme interactive controls such as buttons, switches, and sliders. On web, it generates a primary color scale exposed as CSS variables to the underlying `View`. When omitted, each platform falls back to its default theme.
-```tsx HostSeedColorExample.tsx
+
+
+```tsx
import { Host, Column, Button, Switch } from '@expo/ui';
export default function HostSeedColorExample() {
@@ -160,6 +325,8 @@ export default function HostSeedColorExample() {
}
```
+
+
## API
```tsx
@@ -167,3 +334,5 @@ import { Host } from '@expo/ui';
```
+
+
diff --git a/docs/pages/versions/unversioned/sdk/ui/universal/icon.mdx b/docs/pages/versions/unversioned/sdk/ui/universal/icon.mdx
index d29e583b06cc5f..b4edb2a136315f 100644
--- a/docs/pages/versions/unversioned/sdk/ui/universal/icon.mdx
+++ b/docs/pages/versions/unversioned/sdk/ui/universal/icon.mdx
@@ -8,12 +8,29 @@ platforms: ['android', 'ios', 'expo-go']
import APISection from '~/components/plugins/APISection';
import { APIInstallSection } from '~/components/plugins/InstallSection';
+import { ComponentExample } from '~/ui/components/ComponentExample';
+import { PlatformSpotlight, PlatformTabsGroup } from '~/ui/components/PlatformTabs';
import { Terminal } from '~/ui/components/Snippet';
A platform-native icon. On Android, it renders a Material Symbol XML vector drawable (recommended source: [`@expo/material-symbols`](https://www.npmjs.com/package/@expo/material-symbols)). On iOS, it renders an [SF Symbol](https://developer.apple.com/sf-symbols/).
> **Note:** `Icon` does not render on web.
+
+
+
+
## Installation
@@ -35,7 +52,20 @@ Optionally, install [`@expo/material-symbols`](https://www.npmjs.com/package/@ex
[`Icon.select`](#selectspec) picks the right asset for the current platform. Pair it with [`@expo/ui/babel-plugin`](https://github.com/expo/expo/tree/main/packages/expo-ui/plugin) (auto-loaded by `babel-preset-expo`) so Metro can tree-shake the unused side per platform.
-```tsx IconSelectExample.tsx
+
+
+```tsx
import { Host, Icon } from '@expo/ui';
export default function IconSelectExample() {
@@ -54,11 +84,26 @@ export default function IconSelectExample() {
}
```
+
+
### Hoisted `Icon.select`
Hoist the [`Icon.select`](#selectspec) call when reusing the same icon across multiple call sites.
-```tsx HoistedIconExample.tsx
+
+
+```tsx
import { Host, Row, Icon } from '@expo/ui';
const STAR = Icon.select({
@@ -79,6 +124,8 @@ export default function HoistedIconExample() {
}
```
+
+
### Platform-specific files
Inside an **.android.tsx** file, import the XML asset directly. Inside an **.ios.tsx** file, pass the SF Symbol name as a string.
@@ -115,3 +162,5 @@ import { Icon } from '@expo/ui';
```
+
+
diff --git a/docs/pages/versions/unversioned/sdk/ui/universal/index.mdx b/docs/pages/versions/unversioned/sdk/ui/universal/index.mdx
index 2d0c060f4fa5e9..2ede4b593de5fb 100644
--- a/docs/pages/versions/unversioned/sdk/ui/universal/index.mdx
+++ b/docs/pages/versions/unversioned/sdk/ui/universal/index.mdx
@@ -8,9 +8,14 @@ platforms: ['android', 'ios', 'web', 'expo-go']
---
import { APIInstallSection } from '~/components/plugins/InstallSection';
+import { ComponentExample } from '~/ui/components/ComponentExample';
+import UniversalCards from '~/scenes/versions/unversioned/ui/universal.mdx';
+import { PlatformTabsGroup } from '~/ui/components/PlatformTabs';
The universal components in `@expo/ui` are a single-API layer over the platform-native UI toolkits. On Android, they delegate to [`@expo/ui/jetpack-compose`](../jetpack-compose). On iOS, they delegate to [`@expo/ui/swift-ui`](../swift-ui). On web, they're JS implementations using `react-dom` or `react-native-web` and are picked per component to suit the control.
+
+
## Installation
@@ -19,14 +24,32 @@ The universal components in `@expo/ui` are a single-API layer over the platform-
Universal components must still be wrapped in a [`Host`](host), but you import everything, including `Host`, from the package root. The universal `Host` dispatches to the platform-native host on Android and iOS, so there's no need to reach for [`@expo/ui/swift-ui`](../swift-ui) or [`@expo/ui/jetpack-compose`](../jetpack-compose) directly.
-```tsx UniversalExample.tsx
+
+
+```tsx
+import { useColorScheme } from 'react-native';
import { Host, Column, Button, Text } from '@expo/ui';
-export default function Example() {
+export default function UniversalExample() {
+ const colorScheme = useColorScheme();
+
return (
- Hello, world!
+
+ Hello, world!
+ alert('Pressed')} />
@@ -34,30 +57,15 @@ export default function Example() {
}
```
+
+
## Available components
-| Component | Description |
-| ---------------------------- | --------------------------------------------------------------------------------------------------------- |
-| [`BottomSheet`](bottomsheet) | A modal sheet that slides up from the bottom of the screen. |
-| [`Button`](button) | A pressable button with multiple visual variants. |
-| [`Checkbox`](checkbox) | A toggle control that represents a checked or unchecked state. |
-| [`Collapsible`](collapsible) | A labelled tappable header that toggles visibility of its content. |
-| [`Column`](column) | A vertical layout container for universal @expo/ui components. |
-| [`FieldGroup`](fieldgroup) | A scrollable container of grouped settings-style rows. |
-| [`Host`](host) | A cross-platform Host component that wraps universal @expo/ui content. |
-| [`Icon`](icon) | A platform-native icon — SF Symbol on iOS, Material Symbol on Android. |
-| [`List`](list) | A virtualized vertical container of rows, paired with a tappable ListItem primitive. |
-| [`Picker`](picker) | A single-selection input with menu and wheel appearances. |
-| [`RNHostView`](rnhostview) | A cross-platform component for hosting React Native views inside @expo/ui views. |
-| [`Row`](row) | A horizontal layout container for universal @expo/ui components. |
-| [`ScrollView`](scrollview) | A scrollable container that supports vertical or horizontal scrolling. |
-| [`Slider`](slider) | A control for selecting a value from a continuous or stepped range. |
-| [`Spacer`](spacer) | A layout spacer that produces empty space between siblings. |
-| [`Switch`](switch) | A toggle control that switches between on and off states. |
-| [`Text`](text) | A component for displaying styled text content. |
-| [`TextInput`](textinput) | A text input backed by native SwiftUI and Jetpack Compose components, with a React Native-compatible API. |
+
## When to use this versus `jetpack-compose`/`swift-ui`
- Reach for **universal** components when you want one component tree that runs unmodified on Android, iOS, and web. The platform-native look and feel is preserved on Android and iOS because the components delegate to Jetpack Compose/SwiftUI under the hood.
- Reach for **[`@expo/ui/jetpack-compose`](../jetpack-compose)** or **[`@expo/ui/swift-ui`](../swift-ui)** directly when you need platform-specific controls, modifiers, or behavior that the universal API doesn't surface.
+
+
diff --git a/docs/pages/versions/unversioned/sdk/ui/universal/list.mdx b/docs/pages/versions/unversioned/sdk/ui/universal/list.mdx
index 08b671985f134a..850eecc0cd891c 100644
--- a/docs/pages/versions/unversioned/sdk/ui/universal/list.mdx
+++ b/docs/pages/versions/unversioned/sdk/ui/universal/list.mdx
@@ -8,11 +8,28 @@ platforms: ['android', 'ios', 'web', 'expo-go']
import APISection from '~/components/plugins/APISection';
import { APIInstallSection } from '~/components/plugins/InstallSection';
+import { ComponentExample } from '~/ui/components/ComponentExample';
+import { PlatformSpotlight, PlatformTabsGroup } from '~/ui/components/PlatformTabs';
`List` provides a virtualized vertical container of rows, typically populated with [`ListItem`](#listitem) children. It provides the platform-native chrome (separators, inset styling, pull-to-refresh). `ListItem` is a tappable row with `leading`/`trailing`/`supportingText` slots.
> **info** `List` does not lazily render rows yet: React creates every row up front, so large lists can be slow to mount. We are working on improving this. For large lists, we recommend [FlashList](https://shopify.github.io/flash-list/) or [Legend List](https://github.com/LegendApp/legend-list).
+
+
+
+
## Installation
@@ -21,8 +38,22 @@ import { APIInstallSection } from '~/components/plugins/InstallSection';
### Basic list
-```tsx ListExample.tsx
+
+
+```tsx
import { useState } from 'react';
+import { useColorScheme } from 'react-native';
import { Host, List, ListItem, Text } from '@expo/ui';
const ITEMS = [
@@ -33,6 +64,8 @@ const ITEMS = [
export default function ListExample() {
const [selected, setSelected] = useState(null);
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
return (
@@ -43,17 +76,32 @@ export default function ListExample() {
))}
- {selected != null && Selected: {selected}}
+ {selected != null && {`Selected: ${selected}`}}
);
}
```
+
+
### Rows with slots
`ListItem` accepts `leading`, `trailing`, and `supportingText` shorthand props for the common case. Pass a `ReactNode` for any of them when richer content is needed.
-```tsx ListItemSlotsExample.tsx
+
+
+```tsx
import { Host, Icon, List, ListItem } from '@expo/ui';
const CHEVRON = Icon.select({
@@ -80,24 +128,48 @@ export default function ListItemSlotsExample() {
}
```
+
+
### Compound slot children
For full control over slot content, use the compound API: ``, ``, and ``. Anything not wrapped in a slot becomes the headline.
-```tsx ListItemCompoundExample.tsx
+
+
+```tsx
+import { useColorScheme } from 'react-native';
import { Host, Icon, List, ListItem, Row, Text } from '@expo/ui';
+const STAR = Icon.select({
+ ios: 'star.fill',
+ android: require('@expo/material-symbols/star.xml'),
+});
+
export default function ListItemCompoundExample() {
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
+
return (
{}}>
-
+ {`#42: `}
- Composite headline
+ Composite headlineRicher slot content
@@ -107,11 +179,26 @@ export default function ListItemCompoundExample() {
}
```
+
+
### Pull-to-refresh
Pass an `async` `onRefresh` handler. The platform-native refresh indicator stays visible until the returned promise settles (resolves or rejects).
-```tsx ListRefreshExample.tsx
+
+
+```tsx
import { useState } from 'react';
import { Host, List, ListItem } from '@expo/ui';
@@ -135,6 +222,8 @@ export default function ListRefreshExample() {
}
```
+
+
> Pull-to-refresh is not implemented on web yet. The handler is accepted for API parity but the indicator only appears on Android and iOS.
## API
@@ -146,3 +235,5 @@ import { List, ListItem } from '@expo/ui';
+
+
diff --git a/docs/pages/versions/unversioned/sdk/ui/universal/picker.mdx b/docs/pages/versions/unversioned/sdk/ui/universal/picker.mdx
index 227d4265f08942..ddc62d2e6f2243 100644
--- a/docs/pages/versions/unversioned/sdk/ui/universal/picker.mdx
+++ b/docs/pages/versions/unversioned/sdk/ui/universal/picker.mdx
@@ -8,11 +8,28 @@ platforms: ['android', 'ios', 'web', 'expo-go']
import APISection from '~/components/plugins/APISection';
import { APIInstallSection } from '~/components/plugins/InstallSection';
+import { ComponentExample } from '~/ui/components/ComponentExample';
+import { PlatformSpotlight, PlatformTabsGroup } from '~/ui/components/PlatformTabs';
`Picker` is a single-selection input. You can use `` children to declare options so that the parent `Picker` reads them and renders a platform-appropriate dropdown or rotor.
The universal `Picker` is independent of [`@expo/ui/community/picker`](../drop-in-replacements/picker), which remains a compat shim for `@react-native-picker/picker`. Prefer this universal `Picker` for new code unless you specifically need the RN-Picker API surface.
+
+
+
+
## Installation
@@ -21,8 +38,22 @@ The universal `Picker` is independent of [`@expo/ui/community/picker`](../drop-i
### Menu appearance (default)
-```tsx PickerMenuExample.tsx
+
+
+```tsx
import { useState } from 'react';
+import { useColorScheme } from 'react-native';
import { Host, Row, Picker, Spacer, Text } from '@expo/ui';
const FLAVOURS = [
@@ -33,11 +64,12 @@ const FLAVOURS = [
export default function PickerMenuExample() {
const [value, setValue] = useState('vanilla');
+ const colorScheme = useColorScheme();
return (
-
+
- Flavour:
+ Flavour:
{FLAVOURS.map(f => (
@@ -50,11 +82,26 @@ export default function PickerMenuExample() {
}
```
+
+
### Wheel appearance
`appearance="wheel"` renders an inline scrollable rotor on iOS. On Android and web, this falls back to the platform's default dropdown (Material 3 doesn't ship a wheel-style picker).
-```tsx PickerWheelExample.tsx
+
+
+```tsx
import { useState } from 'react';
import { Host, Column, Picker } from '@expo/ui';
@@ -68,7 +115,7 @@ export default function PickerWheelExample() {
const [value, setValue] = useState('chocolate');
return (
-
+
{FLAVOURS.map(f => (
@@ -81,6 +128,8 @@ export default function PickerWheelExample() {
}
```
+
+
## API
```tsx
@@ -88,3 +137,5 @@ import { Picker } from '@expo/ui';
```
+
+
diff --git a/docs/pages/versions/unversioned/sdk/ui/universal/rnhostview.mdx b/docs/pages/versions/unversioned/sdk/ui/universal/rnhostview.mdx
index 13c9c4389356a2..a25c73b1c2fbf4 100644
--- a/docs/pages/versions/unversioned/sdk/ui/universal/rnhostview.mdx
+++ b/docs/pages/versions/unversioned/sdk/ui/universal/rnhostview.mdx
@@ -8,9 +8,26 @@ platforms: ['android', 'ios', 'web', 'expo-go']
import APISection from '~/components/plugins/APISection';
import { APIInstallSection } from '~/components/plugins/InstallSection';
+import { ComponentExample } from '~/ui/components/ComponentExample';
+import { PlatformSpotlight, PlatformTabsGroup } from '~/ui/components/PlatformTabs';
Hosts a React Native view subtree inside a universal `@expo/ui` layout. On Android and iOS, it re-exports the platform-native [`RNHostView` for Jetpack Compose](../jetpack-compose/rnhostview)/[`RNHostView` for SwiftUI](../swift-ui/rnhostview), so React Native children bridge into the surrounding Compose/SwiftUI tree. On web, there is no native host tree to bridge into, so it falls back to a React Native [`View`](https://reactnative.dev/docs/view) that wraps the children.
+
+
+
+
## Installation
@@ -21,15 +38,33 @@ Hosts a React Native view subtree inside a universal `@expo/ui` layout. On Andro
Place a React Native view subtree anywhere inside a universal `@expo/ui` layout.
-```tsx RNHostViewExample.tsx
+
+
+```tsx
import { Host, Column, RNHostView, Text } from '@expo/ui';
-import { Text as RNText, View } from 'react-native';
+import { Text as RNText, View, useColorScheme } from 'react-native';
export default function RNHostViewExample() {
+ const colorScheme = useColorScheme();
+
return (
- Native UI label
+
+ Native UI label
+
+
### Fill parent vs. match child
By default `RNHostView` fills its native parent. Set `matchContents` to have it shrink to fit its React Native children instead.
-```tsx RNHostViewExample.tsx
+
+
+```tsx
import { Host, Column, Row, Text, RNHostView } from '@expo/ui';
-import { View } from 'react-native';
+import { View, useColorScheme } from 'react-native';
+
+export default function RNHostViewSizingExample() {
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
-export default function RNHostViewExample() {
return (
- Fill parent size
+ Fill parent size
The RNHostView fills the native parent's 100×100 frame.
@@ -72,7 +125,7 @@ export default function RNHostViewExample() {
- Match child size
+ Match child size
The RNHostView shrinks to wrap its 50×50 child.
@@ -90,6 +143,8 @@ export default function RNHostViewExample() {
}
```
+
+
## API
```tsx
@@ -97,3 +152,5 @@ import { RNHostView } from '@expo/ui';
```
+
+
diff --git a/docs/pages/versions/unversioned/sdk/ui/universal/row.mdx b/docs/pages/versions/unversioned/sdk/ui/universal/row.mdx
index e4ae9cf1077cd5..c68e5abad448b7 100644
--- a/docs/pages/versions/unversioned/sdk/ui/universal/row.mdx
+++ b/docs/pages/versions/unversioned/sdk/ui/universal/row.mdx
@@ -8,9 +8,26 @@ platforms: ['android', 'ios', 'web', 'expo-go']
import APISection from '~/components/plugins/APISection';
import { APIInstallSection } from '~/components/plugins/InstallSection';
+import { ComponentExample } from '~/ui/components/ComponentExample';
+import { PlatformSpotlight, PlatformTabsGroup } from '~/ui/components/PlatformTabs';
A horizontal layout container that arranges its children from start to end. Delegates to Jetpack Compose's [`Row`](../jetpack-compose/row) on Android, SwiftUI's [`HStack`](../swift-ui/hstack) on iOS, and a flex `View` on web.
+
+
+
+
## Installation
@@ -19,61 +36,118 @@ A horizontal layout container that arranges its children from start to end. Dele
### Basic row
-```tsx RowExample.tsx
+
+
+```tsx
+import { useColorScheme } from 'react-native';
import { Host, Row, Text } from '@expo/ui';
export default function RowExample() {
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
+
return (
- One
- Two
- Three
+ One
+ Two
+ Three
);
}
```
+
+
### Alignment
Use [`alignment`](#alignment) to position children along the cross (vertical) axis.
-```tsx RowAlignmentExample.tsx
+
+
+```tsx
+import { useColorScheme } from 'react-native';
import { Host, Row, Text } from '@expo/ui';
export default function RowAlignmentExample() {
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
+
return (
-
+
- Centered
- Centered
+ Large
+ centered
);
}
```
+
+
### Pushing content apart with Spacer
Pair `Row` with a flexible [`Spacer`](spacer) to push its children to the opposite ends.
-```tsx RowSpacerExample.tsx
+
+
+```tsx
+import { useColorScheme } from 'react-native';
import { Host, Row, Text, Spacer } from '@expo/ui';
export default function RowSpacerExample() {
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
+
return (
-
+
- Leading
+ Leading
- Trailing
+ Trailing
);
}
```
+
+
## API
```tsx
@@ -81,3 +155,5 @@ import { Row } from '@expo/ui';
```
+
+
diff --git a/docs/pages/versions/unversioned/sdk/ui/universal/scrollview.mdx b/docs/pages/versions/unversioned/sdk/ui/universal/scrollview.mdx
index 170e7f855d0e3e..5aa835dde8512a 100644
--- a/docs/pages/versions/unversioned/sdk/ui/universal/scrollview.mdx
+++ b/docs/pages/versions/unversioned/sdk/ui/universal/scrollview.mdx
@@ -8,9 +8,26 @@ platforms: ['android', 'ios', 'web', 'expo-go']
import APISection from '~/components/plugins/APISection';
import { APIInstallSection } from '~/components/plugins/InstallSection';
+import { ComponentExample } from '~/ui/components/ComponentExample';
+import { PlatformSpotlight, PlatformTabsGroup } from '~/ui/components/PlatformTabs';
A scrollable container, defaulting to vertical scrolling. Use [`direction="horizontal"`](#direction) for horizontal lists.
+
+
+
+
## Installation
@@ -19,16 +36,35 @@ A scrollable container, defaulting to vertical scrolling. Use [`direction="horiz
### Vertical scrolling
-```tsx VerticalScrollViewExample.tsx
+
+
+```tsx
+import { useColorScheme } from 'react-native';
import { Host, ScrollView, Column, Text } from '@expo/ui';
export default function VerticalScrollViewExample() {
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
+
return (
{Array.from({ length: 30 }).map((_, i) => (
- Row {i + 1}
+
+ {`Row ${i + 1}`}
+
))}
@@ -37,18 +73,39 @@ export default function VerticalScrollViewExample() {
}
```
+
+
### Horizontal scrolling
-```tsx HorizontalScrollViewExample.tsx
+
+
+```tsx
+import { useColorScheme } from 'react-native';
import { Host, ScrollView, Row, Text } from '@expo/ui';
export default function HorizontalScrollViewExample() {
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
+
return (
-
+
{Array.from({ length: 20 }).map((_, i) => (
- Item {i + 1}
+
+ {`Item ${i + 1}`}
+
))}
@@ -57,6 +114,8 @@ export default function HorizontalScrollViewExample() {
}
```
+
+
## API
```tsx
@@ -64,3 +123,5 @@ import { ScrollView } from '@expo/ui';
```
+
+
diff --git a/docs/pages/versions/unversioned/sdk/ui/universal/slider.mdx b/docs/pages/versions/unversioned/sdk/ui/universal/slider.mdx
index 2e1a3caab46e9d..431d802d04742c 100644
--- a/docs/pages/versions/unversioned/sdk/ui/universal/slider.mdx
+++ b/docs/pages/versions/unversioned/sdk/ui/universal/slider.mdx
@@ -8,9 +8,26 @@ platforms: ['android', 'ios', 'web', 'expo-go']
import APISection from '~/components/plugins/APISection';
import { APIInstallSection } from '~/components/plugins/InstallSection';
+import { ComponentExample } from '~/ui/components/ComponentExample';
+import { PlatformSpotlight, PlatformTabsGroup } from '~/ui/components/PlatformTabs';
A controlled slider for selecting a numeric value within a range. Pair [`value`](#value) with [`onValueChange`](#onvaluechange) to manage state from React.
+
+
+
+
## Installation
@@ -21,7 +38,20 @@ A controlled slider for selecting a numeric value within a range. Pair [`value`]
The default range is `[0, 1]`.
-```tsx ContinuousSliderExample.tsx
+
+
+```tsx
import { useState } from 'react';
import { Host, Slider } from '@expo/ui';
@@ -29,28 +59,47 @@ export default function ContinuousSliderExample() {
const [value, setValue] = useState(0.5);
return (
-
+
);
}
```
+
+
### Stepped slider with custom range
Use `min`, `max`, and `step` to constrain the values produced by the slider.
-```tsx SteppedSliderExample.tsx
+
+
+```tsx
import { useState } from 'react';
+import { useColorScheme } from 'react-native';
import { Host, Column, Slider, Text } from '@expo/ui';
export default function SteppedSliderExample() {
const [volume, setVolume] = useState(50);
+ const colorScheme = useColorScheme();
return (
-
+
- Volume: {volume}
+
+ {`Volume: ${volume}`}
+
@@ -58,6 +107,8 @@ export default function SteppedSliderExample() {
}
```
+
+
## API
```tsx
@@ -65,3 +116,5 @@ import { Slider } from '@expo/ui';
```
+
+
diff --git a/docs/pages/versions/unversioned/sdk/ui/universal/spacer.mdx b/docs/pages/versions/unversioned/sdk/ui/universal/spacer.mdx
index 229cda9611837c..81418672ceeb75 100644
--- a/docs/pages/versions/unversioned/sdk/ui/universal/spacer.mdx
+++ b/docs/pages/versions/unversioned/sdk/ui/universal/spacer.mdx
@@ -8,9 +8,26 @@ platforms: ['android', 'ios', 'web', 'expo-go']
import APISection from '~/components/plugins/APISection';
import { APIInstallSection } from '~/components/plugins/InstallSection';
+import { ComponentExample } from '~/ui/components/ComponentExample';
+import { PlatformSpotlight, PlatformTabsGroup } from '~/ui/components/PlatformTabs';
A layout spacer that produces empty space between siblings inside a [`Row`](row) or [`Column`](column). Use [`size`](#size) for a fixed gap, or [`flexible`](#flexible) to fill the remaining main-axis space.
+
+
+
+
## Installation
@@ -19,42 +36,80 @@ A layout spacer that produces empty space between siblings inside a [`Row`](row)
### Fixed-size spacer
-```tsx FixedSpacerExample.tsx
+
+
+```tsx
+import { useColorScheme } from 'react-native';
import { Host, Column, Text, Spacer } from '@expo/ui';
export default function FixedSpacerExample() {
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
+
return (
- Top
+ Top
- Bottom
+ Bottom
);
}
```
+
+
### Flexible spacer
A flexible spacer fills the remaining space along its parent's main axis, pushing the surrounding content to opposite ends.
-```tsx FlexibleSpacerExample.tsx
+
+
+```tsx
+import { useColorScheme } from 'react-native';
import { Host, Row, Text, Spacer } from '@expo/ui';
export default function FlexibleSpacerExample() {
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
+
return (
-
+
- Leading
+ Leading
- Trailing
+ Trailing
);
}
```
+
+
## API
```tsx
@@ -62,3 +117,5 @@ import { Spacer } from '@expo/ui';
```
+
+
diff --git a/docs/pages/versions/unversioned/sdk/ui/universal/switch.mdx b/docs/pages/versions/unversioned/sdk/ui/universal/switch.mdx
index 6e8f118a68349b..8434fa8b285887 100644
--- a/docs/pages/versions/unversioned/sdk/ui/universal/switch.mdx
+++ b/docs/pages/versions/unversioned/sdk/ui/universal/switch.mdx
@@ -8,9 +8,26 @@ platforms: ['android', 'ios', 'web', 'expo-go']
import APISection from '~/components/plugins/APISection';
import { APIInstallSection } from '~/components/plugins/InstallSection';
+import { ComponentExample } from '~/ui/components/ComponentExample';
+import { PlatformSpotlight, PlatformTabsGroup } from '~/ui/components/PlatformTabs';
A controlled toggle. Pair [`value`](#value) with [`onValueChange`](#onvaluechange) to manage state from React.
+
+
+
+
## Installation
@@ -19,7 +36,20 @@ A controlled toggle. Pair [`value`](#value) with [`onValueChange`](#onvaluechang
### Basic switch
-```tsx SwitchExample.tsx
+
+
+```tsx
import { useState } from 'react';
import { Host, Switch } from '@expo/ui';
@@ -34,11 +64,26 @@ export default function SwitchExample() {
}
```
+
+
### With label
When `label` is provided, the switch is rendered alongside its text in a labeled row.
-```tsx LabeledSwitchExample.tsx
+
+
+```tsx
import { useState } from 'react';
import { Host, Switch } from '@expo/ui';
@@ -46,13 +91,15 @@ export default function LabeledSwitchExample() {
const [notifications, setNotifications] = useState(true);
return (
-
+
);
}
```
+
+
## API
```tsx
@@ -60,3 +107,5 @@ import { Switch } from '@expo/ui';
```
+
+
diff --git a/docs/pages/versions/unversioned/sdk/ui/universal/text.mdx b/docs/pages/versions/unversioned/sdk/ui/universal/text.mdx
index 1a00cd84998ec3..e5d54a24a178af 100644
--- a/docs/pages/versions/unversioned/sdk/ui/universal/text.mdx
+++ b/docs/pages/versions/unversioned/sdk/ui/universal/text.mdx
@@ -8,9 +8,26 @@ platforms: ['android', 'ios', 'web', 'expo-go']
import APISection from '~/components/plugins/APISection';
import { APIInstallSection } from '~/components/plugins/InstallSection';
+import { ComponentExample } from '~/ui/components/ComponentExample';
+import { PlatformSpotlight, PlatformTabsGroup } from '~/ui/components/PlatformTabs';
A component for displaying text. Adapts to the platform color scheme (light/dark) by default and exposes a focused subset of typography knobs through [`textStyle`](#textstyle).
+
+
+
+
## Installation
@@ -19,45 +36,107 @@ A component for displaying text. Adapts to the platform color scheme (light/dark
### Basic text
-```tsx TextExample.tsx
+
+
+```tsx
+import { useColorScheme } from 'react-native';
import { Host, Text } from '@expo/ui';
export default function TextExample() {
+ const colorScheme = useColorScheme();
+
return (
- Hello, world!
+
+ Hello, world!
+
);
}
```
+
+
### Styled text
Use [`textStyle`](#textstyle) for typography-specific properties (font size, weight, alignment).
-```tsx StyledTextExample.tsx
+
+
+```tsx
+import { useColorScheme } from 'react-native';
import { Host, Text } from '@expo/ui';
export default function StyledTextExample() {
+ const colorScheme = useColorScheme();
+
return (
- Headline
+
+ Headline
+
);
}
```
+
+
### Truncating long text
Use [`numberOfLines`](#numberoflines) to clamp long text with a trailing ellipsis.
-```tsx TruncatedTextExample.tsx
+
+
+```tsx
+import { useColorScheme } from 'react-native';
import { Host, Text } from '@expo/ui';
export default function TruncatedTextExample() {
+ const colorScheme = useColorScheme();
+
return (
-
-
+
+
A very long line of text that will be truncated when it does not fit on a single line.
@@ -65,6 +144,8 @@ export default function TruncatedTextExample() {
}
```
+
+
## API
```tsx
@@ -72,3 +153,5 @@ import { Text } from '@expo/ui';
```
+
+
diff --git a/docs/pages/versions/unversioned/sdk/ui/universal/textinput.mdx b/docs/pages/versions/unversioned/sdk/ui/universal/textinput.mdx
index f30bdb2d3cbc4e..83466d152c9e78 100644
--- a/docs/pages/versions/unversioned/sdk/ui/universal/textinput.mdx
+++ b/docs/pages/versions/unversioned/sdk/ui/universal/textinput.mdx
@@ -8,11 +8,28 @@ platforms: ['android', 'ios', 'web', 'expo-go']
import APISection from '~/components/plugins/APISection';
import { APIInstallSection } from '~/components/plugins/InstallSection';
+import { ComponentExample } from '~/ui/components/ComponentExample';
+import { PlatformSpotlight, PlatformTabsGroup } from '~/ui/components/PlatformTabs';
A text input that routes to [`TextField`](../jetpack-compose/textfield) from `@expo/ui/jetpack-compose` on Android, [`TextField`](../swift-ui/textfield) from `@expo/ui/swift-ui` on iOS, and React Native's [`TextInput`](https://reactnative.dev/docs/textinput) on web.
The API mirrors React Native's [`TextInput`](https://reactnative.dev/docs/textinput), with two changes: [`value`](#value) and [`selection`](#selection) are observable state objects (created with `useNativeState`), and [`onChangeText`](#onchangetext) can be a worklet for synchronously updating the state on the UI thread.
+
+
+
+
## Installation
@@ -23,7 +40,20 @@ The API mirrors React Native's [`TextInput`](https://reactnative.dev/docs/textin
Omit [`value`](#value) and the field manages its own text internally. Use [`onChangeText`](#onchangetext) to observe edits, and use the [ref](#textinputref) for imperative actions like `focus`, `blur`, and `clear`.
-```tsx UncontrolledTextInputExample.tsx
+
+
+```tsx
import { Button, Column, Host, TextInput, type TextInputRef } from '@expo/ui';
import { useRef } from 'react';
@@ -31,7 +61,7 @@ export default function UncontrolledTextInputExample() {
const inputRef = useRef(null);
return (
-
+
+
### Controlled
Pass [`value`](#value) to drive the field from a `useNativeState` observable. The example below replaces `Hello` with `World` as you type.
-```tsx ControlledTextInputExample.tsx
+
+
+```tsx
import { Host, TextInput, useNativeState } from '@expo/ui';
import { useCallback } from 'react';
export default function ControlledTextInputExample() {
- const text = useNativeState('');
+ const text = useNativeState('Hello');
const handleChangeText = useCallback(
(value: string) => {
@@ -66,20 +111,35 @@ export default function ControlledTextInputExample() {
);
return (
-
+
);
}
```
+
+
### Worklet masking
Add the `'worklet'` directive to [`onChangeText`](#onchangetext) for synchronously updating the state on the UI thread. Writes to `value` land without the JS-thread round-trip that can cause cursor flicker.
> **Note:** Worklets require installing [`react-native-worklets`](https://docs.swmansion.com/react-native-worklets/).
-```tsx PhoneMaskExample.tsx
+
+
+```tsx
import { Host, TextInput, useNativeState } from '@expo/ui';
import { useCallback } from 'react';
@@ -92,7 +152,7 @@ function formatPhone(input: string) {
}
export default function PhoneMaskExample() {
- const phone = useNativeState('');
+ const phone = useNativeState('(555) 123-4567');
const selection = useNativeState({ start: 0, end: 0 });
const handleChangeText = useCallback(
@@ -109,7 +169,7 @@ export default function PhoneMaskExample() {
);
return (
-
+
+
## Unsupported React Native props
Some React Native `TextInput` props are not supported, because Compose's `TextField` or SwiftUI's `TextField` does not expose an equivalent, or because the prop is replaced by a different mechanism. See the [API](#api) section below for the supported props. If a missing prop blocks your use case, [open an issue](https://github.com/expo/expo/issues/new/choose) so it can be prioritized.
@@ -133,3 +195,5 @@ import { TextInput, useNativeState } from '@expo/ui';
```
+
+
diff --git a/docs/pages/versions/v54.0.0/sdk/haptics.mdx b/docs/pages/versions/v54.0.0/sdk/haptics.mdx
index b8badd7e7e04ca..dff5180f51cc9c 100644
--- a/docs/pages/versions/v54.0.0/sdk/haptics.mdx
+++ b/docs/pages/versions/v54.0.0/sdk/haptics.mdx
@@ -127,6 +127,9 @@ const styles = StyleSheet.create({
justifyContent: 'center',
paddingHorizontal: 16,
},
+ text: {
+ textAlign: 'center',
+ },
buttonContainer: {
flexDirection: 'row',
alignItems: 'stretch',
diff --git a/docs/pages/versions/v54.0.0/sdk/sqlite.mdx b/docs/pages/versions/v54.0.0/sdk/sqlite.mdx
index cdcf429544b12a..984d1eb751e4b0 100644
--- a/docs/pages/versions/v54.0.0/sdk/sqlite.mdx
+++ b/docs/pages/versions/v54.0.0/sdk/sqlite.mdx
@@ -298,7 +298,7 @@ import { View, Text, StyleSheet } from 'react-native';
export default function App() {
return (
- }>
+ Loading...}>
diff --git a/docs/pages/versions/v55.0.0/sdk/haptics.mdx b/docs/pages/versions/v55.0.0/sdk/haptics.mdx
index fa2c00bb995cee..1f1bb8ae23dd8c 100644
--- a/docs/pages/versions/v55.0.0/sdk/haptics.mdx
+++ b/docs/pages/versions/v55.0.0/sdk/haptics.mdx
@@ -127,6 +127,9 @@ const styles = StyleSheet.create({
justifyContent: 'center',
paddingHorizontal: 16,
},
+ text: {
+ textAlign: 'center',
+ },
buttonContainer: {
flexDirection: 'row',
alignItems: 'stretch',
diff --git a/docs/pages/versions/v55.0.0/sdk/sharing.mdx b/docs/pages/versions/v55.0.0/sdk/sharing.mdx
index 26422926216cac..78d3ceeafb8208 100644
--- a/docs/pages/versions/v55.0.0/sdk/sharing.mdx
+++ b/docs/pages/versions/v55.0.0/sdk/sharing.mdx
@@ -141,6 +141,7 @@ If you are using [React Navigation](https://reactnavigation.org/), you can use t
import * as Linking from 'expo-linking';
import { createStaticNavigation } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
+import HandleShare from './HandleShare';
const RootStack = createNativeStackNavigator({
screens: {
diff --git a/docs/pages/versions/v55.0.0/sdk/sqlite.mdx b/docs/pages/versions/v55.0.0/sdk/sqlite.mdx
index 8e441975260034..528d95fecb5cfe 100644
--- a/docs/pages/versions/v55.0.0/sdk/sqlite.mdx
+++ b/docs/pages/versions/v55.0.0/sdk/sqlite.mdx
@@ -365,7 +365,7 @@ import { View, Text, StyleSheet } from 'react-native';
export default function App() {
return (
- }>
+ Loading...}>
diff --git a/docs/pages/versions/v56.0.0/sdk/haptics.mdx b/docs/pages/versions/v56.0.0/sdk/haptics.mdx
index 245666b9fe806c..8179873d14a21d 100644
--- a/docs/pages/versions/v56.0.0/sdk/haptics.mdx
+++ b/docs/pages/versions/v56.0.0/sdk/haptics.mdx
@@ -127,6 +127,9 @@ const styles = StyleSheet.create({
justifyContent: 'center',
paddingHorizontal: 16,
},
+ text: {
+ textAlign: 'center',
+ },
buttonContainer: {
flexDirection: 'row',
alignItems: 'stretch',
diff --git a/docs/pages/versions/v56.0.0/sdk/sharing.mdx b/docs/pages/versions/v56.0.0/sdk/sharing.mdx
index 7e304b4b08115e..61116bb9d6dd73 100644
--- a/docs/pages/versions/v56.0.0/sdk/sharing.mdx
+++ b/docs/pages/versions/v56.0.0/sdk/sharing.mdx
@@ -141,6 +141,7 @@ If you are using [React Navigation](https://reactnavigation.org/), you can use t
import * as Linking from 'expo-linking';
import { createStaticNavigation } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
+import HandleShare from './HandleShare';
const RootStack = createNativeStackNavigator({
screens: {
diff --git a/docs/pages/versions/v56.0.0/sdk/sqlite.mdx b/docs/pages/versions/v56.0.0/sdk/sqlite.mdx
index 72ea7df1f094c2..726829300ad6ce 100644
--- a/docs/pages/versions/v56.0.0/sdk/sqlite.mdx
+++ b/docs/pages/versions/v56.0.0/sdk/sqlite.mdx
@@ -365,7 +365,7 @@ import { View, Text, StyleSheet } from 'react-native';
export default function App() {
return (
- }>
+ Loading...}>
diff --git a/docs/pages/versions/v57.0.0/sdk/haptics.mdx b/docs/pages/versions/v57.0.0/sdk/haptics.mdx
index c9c2c394ab5ead..93110c21c14a5f 100644
--- a/docs/pages/versions/v57.0.0/sdk/haptics.mdx
+++ b/docs/pages/versions/v57.0.0/sdk/haptics.mdx
@@ -129,6 +129,9 @@ const styles = StyleSheet.create({
justifyContent: 'center',
paddingHorizontal: 16,
},
+ text: {
+ textAlign: 'center',
+ },
buttonContainer: {
flexDirection: 'row',
alignItems: 'stretch',
diff --git a/docs/pages/versions/v57.0.0/sdk/sharing.mdx b/docs/pages/versions/v57.0.0/sdk/sharing.mdx
index 7e4e43f17522b4..a0feb60d1fa8c4 100644
--- a/docs/pages/versions/v57.0.0/sdk/sharing.mdx
+++ b/docs/pages/versions/v57.0.0/sdk/sharing.mdx
@@ -141,6 +141,7 @@ If you are using [React Navigation](https://reactnavigation.org/), you can use t
import * as Linking from 'expo-linking';
import { createStaticNavigation } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
+import HandleShare from './HandleShare';
const RootStack = createNativeStackNavigator({
screens: {
diff --git a/docs/pages/versions/v57.0.0/sdk/sqlite.mdx b/docs/pages/versions/v57.0.0/sdk/sqlite.mdx
index 1f0c6cbc0b125e..cc24e006b9c981 100644
--- a/docs/pages/versions/v57.0.0/sdk/sqlite.mdx
+++ b/docs/pages/versions/v57.0.0/sdk/sqlite.mdx
@@ -365,7 +365,7 @@ import { View, Text, StyleSheet } from 'react-native';
export default function App() {
return (
- }>
+ Loading...}>
diff --git a/docs/pages/versions/v57.0.0/sdk/ui/index.mdx b/docs/pages/versions/v57.0.0/sdk/ui/index.mdx
index 5cb65d63f2e3d3..41b915dadae968 100644
--- a/docs/pages/versions/v57.0.0/sdk/ui/index.mdx
+++ b/docs/pages/versions/v57.0.0/sdk/ui/index.mdx
@@ -10,6 +10,7 @@ hideTOC: true
import JetpackComposeCards from '~/scenes/versions/v57.0.0/ui/jetpack-compose.mdx';
import SwiftUICards from '~/scenes/versions/v57.0.0/ui/swift-ui.mdx';
+import UniversalCards from '~/scenes/versions/v57.0.0/ui/universal.mdx';
`@expo/ui` is a set of native input components that allows you to build fully native interfaces with Jetpack Compose and SwiftUI. It aims to provide the commonly used features and components that a typical app will need.
@@ -50,23 +51,4 @@ See **[Drop-in replacements](drop-in-replacements)** for API-compatible replacem
### Universal
-| Component | Description |
-| -------------------------------------- | --------------------------------------------------------------------------------------------------------- |
-| [`BottomSheet`](universal/bottomsheet) | A modal sheet that slides up from the bottom of the screen. |
-| [`Button`](universal/button) | A pressable button with multiple visual variants. |
-| [`Checkbox`](universal/checkbox) | A toggle control that represents a checked or unchecked state. |
-| [`Collapsible`](universal/collapsible) | A labelled tappable header that toggles visibility of its content. |
-| [`Column`](universal/column) | A vertical layout container for universal @expo/ui components. |
-| [`FieldGroup`](universal/fieldgroup) | A scrollable container of grouped settings-style rows. |
-| [`Host`](universal/host) | A cross-platform Host component that wraps universal @expo/ui content. |
-| [`Icon`](universal/icon) | A platform-native icon — SF Symbol on iOS, Material Symbol on Android. |
-| [`List`](universal/list) | A virtualized vertical container of rows, paired with a tappable ListItem primitive. |
-| [`Picker`](universal/picker) | A single-selection input with menu and wheel appearances. |
-| [`RNHostView`](universal/rnhostview) | A cross-platform component for hosting React Native views inside @expo/ui views. |
-| [`Row`](universal/row) | A horizontal layout container for universal @expo/ui components. |
-| [`ScrollView`](universal/scrollview) | A scrollable container that supports vertical or horizontal scrolling. |
-| [`Slider`](universal/slider) | A control for selecting a value from a continuous or stepped range. |
-| [`Spacer`](universal/spacer) | A layout spacer that produces empty space between siblings. |
-| [`Switch`](universal/switch) | A toggle control that switches between on and off states. |
-| [`Text`](universal/text) | A component for displaying styled text content. |
-| [`TextInput`](universal/textinput) | A text input backed by native SwiftUI and Jetpack Compose components, with a React Native-compatible API. |
+
diff --git a/docs/pages/versions/v57.0.0/sdk/ui/jetpack-compose/modifiers.mdx b/docs/pages/versions/v57.0.0/sdk/ui/jetpack-compose/modifiers.mdx
index f89ef66bae55a8..88b473a2e58f88 100644
--- a/docs/pages/versions/v57.0.0/sdk/ui/jetpack-compose/modifiers.mdx
+++ b/docs/pages/versions/v57.0.0/sdk/ui/jetpack-compose/modifiers.mdx
@@ -530,6 +530,7 @@ Makes the component respond to both short-tap and long-press gestures. Wraps Com
| `options.indication` | `boolean` | Whether to show a ripple indication. Defaults to `true`. |
```tsx
+import { Text } from '@expo/ui/jetpack-compose';
import { combinedClickable } from '@expo/ui/jetpack-compose/modifiers';
+
+
+
## Installation
@@ -19,85 +36,145 @@ A modal sheet that slides up from the bottom of the screen. The sheet's visibili
### Basic bottom sheet
-```tsx BottomSheetExample.tsx
+
+
+```tsx
import { useState } from 'react';
+import { useColorScheme } from 'react-native';
import { Host, Column, Button, BottomSheet, Text } from '@expo/ui';
export default function BottomSheetExample() {
const [isPresented, setIsPresented] = useState(false);
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
return (
-
- setIsPresented(true)} />
+ <>
+
+ setIsPresented(true)} />
+ setIsPresented(false)}>
- Sheet contents
- Drag down or tap the overlay to dismiss.
+ Sheet contents
+ Drag down or tap the overlay to dismiss. setIsPresented(false)} />
-
+ >
);
}
```
+
+
### Hiding the drag indicator
Pass [`showDragIndicator={false}`](#showdragindicator) for sheets without a handle.
-```tsx BottomSheetNoIndicatorExample.tsx
+
+
+```tsx
import { useState } from 'react';
+import { useColorScheme } from 'react-native';
import { Host, Button, BottomSheet, Text } from '@expo/ui';
export default function BottomSheetNoIndicatorExample() {
const [isPresented, setIsPresented] = useState(false);
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
return (
-
- setIsPresented(true)} />
+ <>
+
+ setIsPresented(true)} />
+ setIsPresented(false)}
showDragIndicator={false}>
- No drag handle.
+ No drag handle.
-
+ >
);
}
```
+
+
### Snap points
Pass [`snapPoints`](#snappoints) to let the user drag the sheet between multiple resting heights. You can use the semantic values `'half'` and `'full'` for cross-platform parity. The `{ fraction }` and `{ height }` forms are honored precisely on iOS and web.
When sheet content can be taller than the smallest snap point, wrap it in a `ScrollView` so the overflow scrolls correctly.
-```tsx BottomSheetSnapPointsExample.tsx
+
+
+```tsx
import { useState } from 'react';
+import { useColorScheme } from 'react-native';
import { Host, BottomSheet, Button, Column, ScrollView, Text } from '@expo/ui';
export default function BottomSheetSnapPointsExample() {
const [isPresented, setIsPresented] = useState(false);
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
return (
-
- setIsPresented(true)} />
+ <>
+
+ setIsPresented(true)} />
+ setIsPresented(false)}
snapPoints={['half', 'full']}>
- Half / full sheet
- Drag the sheet between half and full screen height.
+ Half / full sheet
+ Drag the sheet between half and full screen height.
-
+ >
);
}
```
+
+
> On Android, `{ fraction }` and `{ height }` snap to the nearest of `'half'` / `'full'` — the underlying `ModalBottomSheet` only supports two resting states. The partial state is only visible when content is tall enough to exceed Material's partial threshold; give the content an explicit height or fill the available space if you need the half state on short content.
## API
@@ -107,3 +184,5 @@ import { BottomSheet } from '@expo/ui';
```
+
+
diff --git a/docs/pages/versions/v57.0.0/sdk/ui/universal/button.mdx b/docs/pages/versions/v57.0.0/sdk/ui/universal/button.mdx
index 961a7c5fb6e797..4aacb8ba3941f0 100644
--- a/docs/pages/versions/v57.0.0/sdk/ui/universal/button.mdx
+++ b/docs/pages/versions/v57.0.0/sdk/ui/universal/button.mdx
@@ -8,9 +8,26 @@ platforms: ['android', 'ios', 'web', 'expo-go']
import APISection from '~/components/plugins/APISection';
import { APIInstallSection } from '~/components/plugins/InstallSection';
+import { ComponentExample } from '~/ui/components/ComponentExample';
+import { PlatformSpotlight, PlatformTabsGroup } from '~/ui/components/PlatformTabs';
A pressable button that renders consistently across Android, iOS, and web. Supports `filled`, `outlined`, and `text` visual variants.
+
+
+
+
## Installation
@@ -19,7 +36,20 @@ A pressable button that renders consistently across Android, iOS, and web. Suppo
### Basic button
-```tsx BasicButtonExample.tsx
+
+
+```tsx
import { Host, Button } from '@expo/ui';
export default function BasicButtonExample() {
@@ -31,11 +61,26 @@ export default function BasicButtonExample() {
}
```
+
+
### Variants
Pick a visual variant with the [`variant`](#variant) prop.
-```tsx ButtonVariantsExample.tsx
+
+
+```tsx
import { Host, Column, Button } from '@expo/ui';
export default function ButtonVariantsExample() {
@@ -51,11 +96,26 @@ export default function ButtonVariantsExample() {
}
```
+
+
### Custom content
Pass [`children`](#children) for fully custom button contents. The [`label`](#label) prop is ignored when `children` is provided.
-```tsx CustomButtonExample.tsx
+
+
+```tsx
import { Host, Button, Row, Icon, Text } from '@expo/ui';
export default function CustomButtonExample() {
@@ -79,9 +139,24 @@ export default function CustomButtonExample() {
}
```
+
+
### Disabled
-```tsx DisabledButtonExample.tsx
+
+
+```tsx
import { Host, Button } from '@expo/ui';
export default function DisabledButtonExample() {
@@ -93,6 +168,8 @@ export default function DisabledButtonExample() {
}
```
+
+
## API
```tsx
@@ -100,3 +177,5 @@ import { Button } from '@expo/ui';
```
+
+
diff --git a/docs/pages/versions/v57.0.0/sdk/ui/universal/checkbox.mdx b/docs/pages/versions/v57.0.0/sdk/ui/universal/checkbox.mdx
index 1453b251be1d80..d638d67ab2b02d 100644
--- a/docs/pages/versions/v57.0.0/sdk/ui/universal/checkbox.mdx
+++ b/docs/pages/versions/v57.0.0/sdk/ui/universal/checkbox.mdx
@@ -8,9 +8,26 @@ platforms: ['android', 'ios', 'web', 'expo-go']
import APISection from '~/components/plugins/APISection';
import { APIInstallSection } from '~/components/plugins/InstallSection';
+import { ComponentExample } from '~/ui/components/ComponentExample';
+import { PlatformSpotlight, PlatformTabsGroup } from '~/ui/components/PlatformTabs';
A controlled checkbox. Pair [`value`](#value) with [`onValueChange`](#onvaluechange) to manage state from React.
+
+
+
+
## Installation
@@ -19,7 +36,20 @@ A controlled checkbox. Pair [`value`](#value) with [`onValueChange`](#onvaluecha
### Basic checkbox
-```tsx CheckboxExample.tsx
+
+
+```tsx
import { useState } from 'react';
import { Host, Checkbox } from '@expo/ui';
@@ -34,9 +64,24 @@ export default function CheckboxExample() {
}
```
+
+
### Disabled
-```tsx DisabledCheckboxExample.tsx
+
+
+```tsx
import { Host, Checkbox } from '@expo/ui';
export default function DisabledCheckboxExample() {
@@ -48,6 +93,8 @@ export default function DisabledCheckboxExample() {
}
```
+
+
## API
```tsx
@@ -55,3 +102,5 @@ import { Checkbox } from '@expo/ui';
```
+
+
diff --git a/docs/pages/versions/v57.0.0/sdk/ui/universal/collapsible.mdx b/docs/pages/versions/v57.0.0/sdk/ui/universal/collapsible.mdx
index c8292d48d1e56d..48758b83239238 100644
--- a/docs/pages/versions/v57.0.0/sdk/ui/universal/collapsible.mdx
+++ b/docs/pages/versions/v57.0.0/sdk/ui/universal/collapsible.mdx
@@ -8,9 +8,26 @@ platforms: ['android', 'ios', 'web', 'expo-go']
import APISection from '~/components/plugins/APISection';
import { APIInstallSection } from '~/components/plugins/InstallSection';
+import { ComponentExample } from '~/ui/components/ComponentExample';
+import { PlatformSpotlight, PlatformTabsGroup } from '~/ui/components/PlatformTabs';
`Collapsible` is a primitive that shows or hides its content with a tap on a labelled header. Controlled via [`isOpen`](#isopen) and [`onOpenChange`](#onopenchange) — each `Collapsible` manages independent state.
+
+
+
+
## Installation
@@ -19,18 +36,34 @@ import { APIInstallSection } from '~/components/plugins/InstallSection';
### Basic collapsible
-```tsx CollapsibleExample.tsx
+
+
+```tsx
import { useState } from 'react';
+import { useColorScheme } from 'react-native';
import { Host, Column, Collapsible, Text } from '@expo/ui';
export default function CollapsibleExample() {
const [open, setOpen] = useState(false);
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
return (
-
+
-
+
A primitive that toggles visibility of its content via a labelled tappable header.
@@ -40,39 +73,57 @@ export default function CollapsibleExample() {
}
```
+
+
### Accordion (one section open at a time)
Wire each `Collapsible`'s `isOpen` to a shared parent value. The component doesn't enforce exclusivity — composition is up to the consumer.
-```tsx CollapsibleAccordionExample.tsx
+
+
+```tsx
import { useState } from 'react';
+import { useColorScheme } from 'react-native';
import { Host, Column, Collapsible, Text } from '@expo/ui';
type Section = 'a' | 'b' | 'c' | null;
export default function CollapsibleAccordionExample() {
const [openSection, setOpenSection] = useState('a');
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
return (
-
+ setOpenSection(open ? 'a' : null)}
label="Section A">
- Opening B or C closes this one.
+ Opening B or C closes this one. setOpenSection(open ? 'b' : null)}
label="Section B">
- Opening A or C closes this one.
+ Opening A or C closes this one. setOpenSection(open ? 'c' : null)}
label="Section C">
- Opening A or B closes this one.
+ Opening A or B closes this one.
@@ -80,6 +131,8 @@ export default function CollapsibleAccordionExample() {
}
```
+
+
## API
```tsx
@@ -87,3 +140,5 @@ import { Collapsible } from '@expo/ui';
```
+
+
diff --git a/docs/pages/versions/v57.0.0/sdk/ui/universal/column.mdx b/docs/pages/versions/v57.0.0/sdk/ui/universal/column.mdx
index 41fb8e834db495..fc79c57b7aefda 100644
--- a/docs/pages/versions/v57.0.0/sdk/ui/universal/column.mdx
+++ b/docs/pages/versions/v57.0.0/sdk/ui/universal/column.mdx
@@ -8,9 +8,26 @@ platforms: ['android', 'ios', 'web', 'expo-go']
import APISection from '~/components/plugins/APISection';
import { APIInstallSection } from '~/components/plugins/InstallSection';
+import { ComponentExample } from '~/ui/components/ComponentExample';
+import { PlatformSpotlight, PlatformTabsGroup } from '~/ui/components/PlatformTabs';
A vertical layout container that arranges its children from top to bottom. Delegates to SwiftUI's [`VStack`](../swift-ui/vstack) on iOS, Jetpack Compose's [`Column`](../jetpack-compose/column) on Android, and a flex `View` on web.
+
+
+
+
## Installation
@@ -19,41 +36,79 @@ A vertical layout container that arranges its children from top to bottom. Deleg
### Basic column
-```tsx ColumnExample.tsx
+
+
+```tsx
+import { useColorScheme } from 'react-native';
import { Host, Column, Text } from '@expo/ui';
export default function ColumnExample() {
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
+
return (
- First
- Second
- Third
+ First
+ Second
+ Third
);
}
```
+
+
### Alignment
Use [`alignment`](#alignment) to position children along the cross (horizontal) axis.
-```tsx ColumnAlignmentExample.tsx
+
+
+```tsx
+import { useColorScheme } from 'react-native';
import { Host, Column, Text } from '@expo/ui';
export default function ColumnAlignmentExample() {
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
+
return (
-
+
- Centered
- Centered
+ Centered
+ Centered
);
}
```
+
+
## API
```tsx
@@ -61,3 +116,5 @@ import { Column } from '@expo/ui';
```
+
+
diff --git a/docs/pages/versions/v57.0.0/sdk/ui/universal/fieldgroup.mdx b/docs/pages/versions/v57.0.0/sdk/ui/universal/fieldgroup.mdx
index 2a405e94aad59d..ebdf3344811cb0 100644
--- a/docs/pages/versions/v57.0.0/sdk/ui/universal/fieldgroup.mdx
+++ b/docs/pages/versions/v57.0.0/sdk/ui/universal/fieldgroup.mdx
@@ -8,9 +8,26 @@ platforms: ['android', 'ios', 'web', 'expo-go']
import APISection from '~/components/plugins/APISection';
import { APIInstallSection } from '~/components/plugins/InstallSection';
+import { ComponentExample } from '~/ui/components/ComponentExample';
+import { PlatformSpotlight, PlatformTabsGroup } from '~/ui/components/PlatformTabs';
A scrollable container for grouped settings-style rows, mirroring the look of an iOS Settings screen. Compose `FieldGroup.Section` (for explicit groups), `FieldGroup.SectionHeader`, and `FieldGroup.SectionFooter` slots inside.
+
+
+
+
## Installation
@@ -19,13 +36,29 @@ A scrollable container for grouped settings-style rows, mirroring the look of an
### Sectioned form
-```tsx FieldGroupExample.tsx
+
+
+```tsx
import { useState } from 'react';
+import { useColorScheme } from 'react-native';
import { Host, FieldGroup, Switch, Text } from '@expo/ui';
export default function FieldGroupExample() {
const [notifications, setNotifications] = useState(true);
const [analytics, setAnalytics] = useState(false);
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
return (
@@ -36,7 +69,7 @@ export default function FieldGroupExample() {
- Version 1.0.0
+ Version 1.0.0
@@ -44,23 +77,41 @@ export default function FieldGroupExample() {
}
```
+
+
### Custom section header and footer
Use `FieldGroup.SectionHeader` and `FieldGroup.SectionFooter` to render fully styled header/footer slots in place of the default `title` text.
-```tsx FieldGroupSlotsExample.tsx
+
+
+```tsx
import { useState } from 'react';
+import { useColorScheme } from 'react-native';
import { Host, FieldGroup, Switch, Text } from '@expo/ui';
export default function FieldGroupSlotsExample() {
const [enabled, setEnabled] = useState(false);
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
return (
- Privacy
+ Privacy
@@ -77,6 +128,8 @@ export default function FieldGroupSlotsExample() {
}
```
+
+
## API
```tsx
@@ -84,3 +137,5 @@ import { FieldGroup } from '@expo/ui';
```
+
+
diff --git a/docs/pages/versions/v57.0.0/sdk/ui/universal/host.mdx b/docs/pages/versions/v57.0.0/sdk/ui/universal/host.mdx
index f4467f91597326..574f6886a45203 100644
--- a/docs/pages/versions/v57.0.0/sdk/ui/universal/host.mdx
+++ b/docs/pages/versions/v57.0.0/sdk/ui/universal/host.mdx
@@ -8,9 +8,26 @@ platforms: ['android', 'ios', 'web', 'expo-go']
import APISection from '~/components/plugins/APISection';
import { APIInstallSection } from '~/components/plugins/InstallSection';
+import { ComponentExample } from '~/ui/components/ComponentExample';
+import { PlatformSpotlight, PlatformTabsGroup } from '~/ui/components/PlatformTabs';
A container for universal `@expo/ui` content. On Android and iOS it re-exports the platform-native [`Host` for Jetpack Compose](../jetpack-compose/host)/[`Host` for SwiftUI](../swift-ui/host), so Jetpack Compose/SwiftUI children render exactly as they would in the platform-specific packages. On web, it falls back to a React Native [`View`](https://reactnative.dev/docs/view). Use `Host` as the root of any universal subtree so the same component tree works across all three platforms.
+
+
+
+
## Installation
@@ -19,14 +36,32 @@ A container for universal `@expo/ui` content. On Android and iOS it re-exports t
### Basic usage
-```tsx HostExample.tsx
+
+
+```tsx
+import { useColorScheme } from 'react-native';
import { Host, Column, Text, Button } from '@expo/ui';
export default function HostExample() {
+ const colorScheme = useColorScheme();
+
return (
- Hello, world!
+
+ Hello, world!
+ alert('Pressed')} />
@@ -34,13 +69,28 @@ export default function HostExample() {
}
```
+
+
### Match contents sizing
Use `matchContents` to let `Host` size itself to fit its content. On Android and iOS, this is forwarded to the platform-native `Host` (see [Jetpack Compose](../jetpack-compose/host)/[SwiftUI](../swift-ui/host) for the exact platform semantics). On web, it applies `alignSelf: 'flex-start'` to the underlying `View` so the host shrinks to fit its children instead of being stretched by its parent.
> **Note:** On web, the per-axis form (`{ horizontal: true }` / `{ vertical: true }`) behaves the same as the boolean form, since `alignSelf` only controls stretching on the parent's cross axis. Components that rely on independent per-axis sizing should expect the same shrink-to-fit behavior on web regardless of which axis is opted in.
-```tsx MatchContentsExample.tsx
+
+
+```tsx
import { Host, Button } from '@expo/ui';
export default function MatchContentsExample() {
@@ -52,95 +102,197 @@ export default function MatchContentsExample() {
}
```
+
+
### Layout direction
Use `layoutDirection` to render the subtree as left-to-right or right-to-left. On Android and iOS, this is forwarded to the platform-native `Host` (see [Jetpack Compose](../jetpack-compose/host)/[SwiftUI](../swift-ui/host) for the exact platform semantics). On web, it sets the `dir` attribute on the underlying `View` so descendants inherit the chosen direction.
-```tsx LayoutDirectionExample.tsx
+
+
+```tsx
+import { useColorScheme } from 'react-native';
import { Host, Row, Text } from '@expo/ui';
export default function LayoutDirectionExample() {
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
+
return (
-
+
- First
- Second
+ First
+ Second
);
}
```
+
+
### Reacting to content layout
Use `onLayoutContent` to be notified of the current dimensions of the host's content. On Android and iOS, this is forwarded to the platform-native `Host` (see [Jetpack Compose](../jetpack-compose/host)/[SwiftUI](../swift-ui/host) for the exact platform semantics). On web, it is derived from the underlying `View`'s `onLayout` callback.
-```tsx OnLayoutContentExample.tsx
+
+
+```tsx
+import { useColorScheme } from 'react-native';
import { Host, Text } from '@expo/ui';
export default function OnLayoutContentExample() {
+ const colorScheme = useColorScheme();
+
return (
console.log(`content size: ${width}x${height}`)
}>
- Hello, world!
+
+ Hello, world!
+
);
}
```
+
+
### Filling the viewport
Use `useViewportSizeMeasurement` for content that should size to the available viewport space. On Android and iOS, this is forwarded to the platform-native `Host` (see [Jetpack Compose](../jetpack-compose/host)/[SwiftUI](../swift-ui/host) for the exact platform semantics). On web, the host's underlying `View` is given the current window's width and height; any explicit `style` you pass still wins.
-```tsx UseViewportSizeMeasurementExample.tsx
+
+
+```tsx
+import { useColorScheme } from 'react-native';
import { Host, Column, Text } from '@expo/ui';
export default function UseViewportSizeMeasurementExample() {
+ const colorScheme = useColorScheme();
+
return (
- Fills the viewport
+
+ Fills the viewport
+
);
}
```
+
+
### Ignoring safe areas
By default, `Host` respects the device safe area insets (notch, home indicator, and so on). Use `ignoreSafeArea="all"` to let content extend edge-to-edge, or `ignoreSafeArea="keyboard"` to keep safe-area padding but ignore the keyboard inset. On Android and iOS, this is forwarded to the platform-native `Host` (see [Jetpack Compose](../jetpack-compose/host)/[SwiftUI](../swift-ui/host) for the exact platform semantics). On web, it is implemented via the CSS `env(safe-area-inset-*)` values applied as padding on the underlying `View`; the default also folds in `env(keyboard-inset-*)` for pages that opt in to the [VirtualKeyboard API](https://developer.mozilla.org/en-US/docs/Web/API/VirtualKeyboard_API).
-```tsx IgnoreSafeAreaExample.tsx
-import { Host, Text } from '@expo/ui';
+
+
+```tsx
+import { useColorScheme } from 'react-native';
+import { Host, Column, Spacer, Text } from '@expo/ui';
export default function IgnoreSafeAreaExample() {
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
+
return (
-
- Extends behind the notch and home indicator
+
+
+ Behind the status bar
+
+ Behind the home indicator
+
);
}
```
+
+
### Forcing a color scheme
Use `colorScheme` to override the appearance of descendant native views. Pass `'light'` or `'dark'` to force one, or omit it to follow the device setting. Android and iOS only — ignored on web.
-```tsx HostColorSchemeExample.tsx
+
+
+```tsx
import { Host, Button } from '@expo/ui';
export default function HostColorSchemeExample() {
return (
-
+ {}} />
);
}
```
+
+
## API
```tsx
@@ -148,3 +300,5 @@ import { Host } from '@expo/ui';
```
+
+
diff --git a/docs/pages/versions/v57.0.0/sdk/ui/universal/icon.mdx b/docs/pages/versions/v57.0.0/sdk/ui/universal/icon.mdx
index f49abd9a5d4e17..1a6a420412c532 100644
--- a/docs/pages/versions/v57.0.0/sdk/ui/universal/icon.mdx
+++ b/docs/pages/versions/v57.0.0/sdk/ui/universal/icon.mdx
@@ -8,12 +8,29 @@ platforms: ['android', 'ios', 'expo-go']
import APISection from '~/components/plugins/APISection';
import { APIInstallSection } from '~/components/plugins/InstallSection';
+import { ComponentExample } from '~/ui/components/ComponentExample';
+import { PlatformSpotlight, PlatformTabsGroup } from '~/ui/components/PlatformTabs';
import { Terminal } from '~/ui/components/Snippet';
A platform-native icon. On Android, it renders a Material Symbol XML vector drawable (recommended source: [`@expo/material-symbols`](https://www.npmjs.com/package/@expo/material-symbols)). On iOS, it renders an [SF Symbol](https://developer.apple.com/sf-symbols/).
> **Note:** `Icon` does not render on web.
+
+
+
+
## Installation
@@ -28,7 +45,20 @@ Optionally, install [`@expo/material-symbols`](https://www.npmjs.com/package/@ex
[`Icon.select`](#selectspec) picks the right asset for the current platform. Pair it with [`@expo/ui/babel-plugin`](https://github.com/expo/expo/tree/main/packages/expo-ui/plugin) (auto-loaded by `babel-preset-expo`) so Metro can tree-shake the unused side per platform.
-```tsx IconSelectExample.tsx
+
+
+```tsx
import { Host, Icon } from '@expo/ui';
export default function IconSelectExample() {
@@ -47,11 +77,26 @@ export default function IconSelectExample() {
}
```
+
+
### Hoisted `Icon.select`
Hoist the [`Icon.select`](#selectspec) call when reusing the same icon across multiple call sites.
-```tsx HoistedIconExample.tsx
+
+
+```tsx
import { Host, Row, Icon } from '@expo/ui';
const STAR = Icon.select({
@@ -72,6 +117,8 @@ export default function HoistedIconExample() {
}
```
+
+
### Platform-specific files
Inside an **.android.tsx** file, import the XML asset directly. Inside an **.ios.tsx** file, pass the SF Symbol name as a string.
@@ -108,3 +155,5 @@ import { Icon } from '@expo/ui';
```
+
+
diff --git a/docs/pages/versions/v57.0.0/sdk/ui/universal/index.mdx b/docs/pages/versions/v57.0.0/sdk/ui/universal/index.mdx
index 3821481ae63fb6..0a7aeeef69ed25 100644
--- a/docs/pages/versions/v57.0.0/sdk/ui/universal/index.mdx
+++ b/docs/pages/versions/v57.0.0/sdk/ui/universal/index.mdx
@@ -9,9 +9,14 @@ hideTOC: true
---
import { APIInstallSection } from '~/components/plugins/InstallSection';
+import { ComponentExample } from '~/ui/components/ComponentExample';
+import UniversalCards from '~/scenes/versions/v57.0.0/ui/universal.mdx';
+import { PlatformTabsGroup } from '~/ui/components/PlatformTabs';
The universal components in `@expo/ui` are a single-API layer over the platform-native UI toolkits. On Android, they delegate to [`@expo/ui/jetpack-compose`](../jetpack-compose). On iOS, they delegate to [`@expo/ui/swift-ui`](../swift-ui). On web, they're JS implementations using `react-dom` or `react-native-web` and are picked per component to suit the control.
+
+
## Installation
@@ -20,14 +25,32 @@ The universal components in `@expo/ui` are a single-API layer over the platform-
Universal components must still be wrapped in a [`Host`](host), but you import everything, including `Host`, from the package root. The universal `Host` dispatches to the platform-native host on Android and iOS, so there's no need to reach for [`@expo/ui/swift-ui`](../swift-ui) or [`@expo/ui/jetpack-compose`](../jetpack-compose) directly.
-```tsx UniversalExample.tsx
+
+
+```tsx
+import { useColorScheme } from 'react-native';
import { Host, Column, Button, Text } from '@expo/ui';
export default function Example() {
+ const colorScheme = useColorScheme();
+
return (
- Hello, world!
+
+ Hello, world!
+ alert('Pressed')} />
@@ -35,30 +58,15 @@ export default function Example() {
}
```
+
+
## Components
-| Component | Description |
-| ---------------------------- | --------------------------------------------------------------------------------------------------------- |
-| [`BottomSheet`](bottomsheet) | A modal sheet that slides up from the bottom of the screen. |
-| [`Button`](button) | A pressable button with multiple visual variants. |
-| [`Checkbox`](checkbox) | A toggle control that represents a checked or unchecked state. |
-| [`Collapsible`](collapsible) | A labelled tappable header that toggles visibility of its content. |
-| [`Column`](column) | A vertical layout container for universal @expo/ui components. |
-| [`FieldGroup`](fieldgroup) | A scrollable container of grouped settings-style rows. |
-| [`Host`](host) | A cross-platform Host component that wraps universal @expo/ui content. |
-| [`Icon`](icon) | A platform-native icon — SF Symbol on iOS, Material Symbol on Android. |
-| [`List`](list) | A virtualized vertical container of rows, paired with a tappable ListItem primitive. |
-| [`Picker`](picker) | A single-selection input with menu and wheel appearances. |
-| [`RNHostView`](rnhostview) | A cross-platform component for hosting React Native views inside @expo/ui views. |
-| [`Row`](row) | A horizontal layout container for universal @expo/ui components. |
-| [`ScrollView`](scrollview) | A scrollable container that supports vertical or horizontal scrolling. |
-| [`Slider`](slider) | A control for selecting a value from a continuous or stepped range. |
-| [`Spacer`](spacer) | A layout spacer that produces empty space between siblings. |
-| [`Switch`](switch) | A toggle control that switches between on and off states. |
-| [`Text`](text) | A component for displaying styled text content. |
-| [`TextInput`](textinput) | A text input backed by native SwiftUI and Jetpack Compose components, with a React Native-compatible API. |
+
## When to use this versus `swift-ui` / `jetpack-compose`
- Reach for **universal** components when you want one component tree that runs unmodified on Android, iOS, and web. The platform-native look and feel is preserved on Android and iOS because the components delegate to Jetpack Compose/SwiftUI under the hood.
- Reach for **[`@expo/ui/swift-ui`](../swift-ui)** or **[`@expo/ui/jetpack-compose`](../jetpack-compose)** directly when you need platform-specific controls, modifiers, or behavior that the universal API doesn't surface.
+
+
diff --git a/docs/pages/versions/v57.0.0/sdk/ui/universal/list.mdx b/docs/pages/versions/v57.0.0/sdk/ui/universal/list.mdx
index 239585b6a7cdb0..bd733576068073 100644
--- a/docs/pages/versions/v57.0.0/sdk/ui/universal/list.mdx
+++ b/docs/pages/versions/v57.0.0/sdk/ui/universal/list.mdx
@@ -8,11 +8,28 @@ platforms: ['android', 'ios', 'web', 'expo-go']
import APISection from '~/components/plugins/APISection';
import { APIInstallSection } from '~/components/plugins/InstallSection';
+import { ComponentExample } from '~/ui/components/ComponentExample';
+import { PlatformSpotlight, PlatformTabsGroup } from '~/ui/components/PlatformTabs';
`List` provides a virtualized vertical container of rows, typically populated with [`ListItem`](#listitem) children. It provides the platform-native chrome (separators, inset styling, pull-to-refresh). `ListItem` is a tappable row with `leading`/`trailing`/`supportingText` slots.
> **info** `List` does not lazily render rows yet: React creates every row up front, so large lists can be slow to mount. We are working on improving this. For large lists, we recommend [FlashList](https://shopify.github.io/flash-list/) or [Legend List](https://github.com/LegendApp/legend-list).
+
+
+
+
## Installation
@@ -21,8 +38,22 @@ import { APIInstallSection } from '~/components/plugins/InstallSection';
### Basic list
-```tsx ListExample.tsx
+
+
+```tsx
import { useState } from 'react';
+import { useColorScheme } from 'react-native';
import { Host, List, ListItem, Text } from '@expo/ui';
const ITEMS = [
@@ -33,6 +64,8 @@ const ITEMS = [
export default function ListExample() {
const [selected, setSelected] = useState(null);
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
return (
@@ -43,17 +76,32 @@ export default function ListExample() {
))}
- {selected != null && Selected: {selected}}
+ {selected != null && {`Selected: ${selected}`}}
);
}
```
+
+
### Rows with slots
`ListItem` accepts `leading`, `trailing`, and `supportingText` shorthand props for the common case. Pass a `ReactNode` for any of them when richer content is needed.
-```tsx ListItemSlotsExample.tsx
+
+
+```tsx
import { Host, Icon, List, ListItem } from '@expo/ui';
const CHEVRON = Icon.select({
@@ -80,24 +128,48 @@ export default function ListItemSlotsExample() {
}
```
+
+
### Compound slot children
For full control over slot content, use the compound API: ``, ``, and ``. Anything not wrapped in a slot becomes the headline.
-```tsx ListItemCompoundExample.tsx
+
+
+```tsx
+import { useColorScheme } from 'react-native';
import { Host, Icon, List, ListItem, Row, Text } from '@expo/ui';
+const STAR = Icon.select({
+ ios: 'star.fill',
+ android: require('@expo/material-symbols/star.xml'),
+});
+
export default function ListItemCompoundExample() {
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
+
return (
{}}>
-
+ {`#42: `}
- Composite headline
+ Composite headlineRicher slot content
@@ -107,11 +179,26 @@ export default function ListItemCompoundExample() {
}
```
+
+
### Pull-to-refresh
Pass an `async` `onRefresh` handler. The platform-native refresh indicator stays visible until the returned promise settles (resolves or rejects).
-```tsx ListRefreshExample.tsx
+
+
+```tsx
import { useState } from 'react';
import { Host, List, ListItem } from '@expo/ui';
@@ -135,6 +222,8 @@ export default function ListRefreshExample() {
}
```
+
+
> Pull-to-refresh is not implemented on web yet. The handler is accepted for API parity but the indicator only appears on Android and iOS.
## API
@@ -146,3 +235,5 @@ import { List, ListItem } from '@expo/ui';
+
+
diff --git a/docs/pages/versions/v57.0.0/sdk/ui/universal/picker.mdx b/docs/pages/versions/v57.0.0/sdk/ui/universal/picker.mdx
index bc203a5aaacec5..cf9538b4daa919 100644
--- a/docs/pages/versions/v57.0.0/sdk/ui/universal/picker.mdx
+++ b/docs/pages/versions/v57.0.0/sdk/ui/universal/picker.mdx
@@ -8,11 +8,28 @@ platforms: ['android', 'ios', 'web', 'expo-go']
import APISection from '~/components/plugins/APISection';
import { APIInstallSection } from '~/components/plugins/InstallSection';
+import { ComponentExample } from '~/ui/components/ComponentExample';
+import { PlatformSpotlight, PlatformTabsGroup } from '~/ui/components/PlatformTabs';
`Picker` is a single-selection input. You can use `` children to declare options so that the parent `Picker` reads them and renders a platform-appropriate dropdown or rotor.
The universal `Picker` is independent of [`@expo/ui/community/picker`](../drop-in-replacements/picker), which remains a compat shim for `@react-native-picker/picker`. Prefer this universal `Picker` for new code unless you specifically need the RN-Picker API surface.
+
+
+
+
## Installation
@@ -21,8 +38,22 @@ The universal `Picker` is independent of [`@expo/ui/community/picker`](../drop-i
### Menu appearance (default)
-```tsx PickerMenuExample.tsx
+
+
+```tsx
import { useState } from 'react';
+import { useColorScheme } from 'react-native';
import { Host, Row, Picker, Spacer, Text } from '@expo/ui';
const FLAVOURS = [
@@ -33,11 +64,12 @@ const FLAVOURS = [
export default function PickerMenuExample() {
const [value, setValue] = useState('vanilla');
+ const colorScheme = useColorScheme();
return (
-
+
- Flavour:
+ Flavour:
{FLAVOURS.map(f => (
@@ -50,11 +82,26 @@ export default function PickerMenuExample() {
}
```
+
+
### Wheel appearance
`appearance="wheel"` renders an inline scrollable rotor on iOS. On Android and web, this falls back to the platform's default dropdown (Material 3 doesn't ship a wheel-style picker).
-```tsx PickerWheelExample.tsx
+
+
+```tsx
import { useState } from 'react';
import { Host, Column, Picker } from '@expo/ui';
@@ -68,7 +115,7 @@ export default function PickerWheelExample() {
const [value, setValue] = useState('chocolate');
return (
-
+
{FLAVOURS.map(f => (
@@ -81,6 +128,8 @@ export default function PickerWheelExample() {
}
```
+
+
## API
```tsx
@@ -88,3 +137,5 @@ import { Picker } from '@expo/ui';
```
+
+
diff --git a/docs/pages/versions/v57.0.0/sdk/ui/universal/rnhostview.mdx b/docs/pages/versions/v57.0.0/sdk/ui/universal/rnhostview.mdx
index 684f414780d404..072a47d68c6055 100644
--- a/docs/pages/versions/v57.0.0/sdk/ui/universal/rnhostview.mdx
+++ b/docs/pages/versions/v57.0.0/sdk/ui/universal/rnhostview.mdx
@@ -8,9 +8,26 @@ platforms: ['android', 'ios', 'web', 'expo-go']
import APISection from '~/components/plugins/APISection';
import { APIInstallSection } from '~/components/plugins/InstallSection';
+import { ComponentExample } from '~/ui/components/ComponentExample';
+import { PlatformSpotlight, PlatformTabsGroup } from '~/ui/components/PlatformTabs';
Hosts a React Native view subtree inside a universal `@expo/ui` layout. On Android and iOS, it re-exports the platform-native [`RNHostView` for Jetpack Compose](../jetpack-compose/rnhostview)/[`RNHostView` for SwiftUI](../swift-ui/rnhostview), so React Native children bridge into the surrounding Compose/SwiftUI tree. On web, there is no native host tree to bridge into, so it falls back to a React Native [`View`](https://reactnative.dev/docs/view) that wraps the children.
+
+
+
+
## Installation
@@ -21,15 +38,33 @@ Hosts a React Native view subtree inside a universal `@expo/ui` layout. On Andro
Place a React Native view subtree anywhere inside a universal `@expo/ui` layout.
-```tsx RNHostViewExample.tsx
+
+
+```tsx
import { Host, Column, RNHostView, Text } from '@expo/ui';
-import { Text as RNText, View } from 'react-native';
+import { Text as RNText, View, useColorScheme } from 'react-native';
export default function RNHostViewExample() {
+ const colorScheme = useColorScheme();
+
return (
- Native UI label
+
+ Native UI label
+
+
### Fill parent vs. match child
By default `RNHostView` fills its native parent. Set `matchContents` to have it shrink to fit its React Native children instead.
-```tsx RNHostViewExample.tsx
+
+
+```tsx
import { Host, Column, Row, Text, RNHostView } from '@expo/ui';
-import { View } from 'react-native';
+import { View, useColorScheme } from 'react-native';
+
+export default function RNHostViewSizingExample() {
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
-export default function RNHostViewExample() {
return (
- Fill parent size
+ Fill parent size
The RNHostView fills the native parent's 100×100 frame.
@@ -72,7 +125,7 @@ export default function RNHostViewExample() {
- Match child size
+ Match child size
The RNHostView shrinks to wrap its 50×50 child.
@@ -90,6 +143,8 @@ export default function RNHostViewExample() {
}
```
+
+
## API
```tsx
@@ -97,3 +152,5 @@ import { RNHostView } from '@expo/ui';
```
+
+
diff --git a/docs/pages/versions/v57.0.0/sdk/ui/universal/row.mdx b/docs/pages/versions/v57.0.0/sdk/ui/universal/row.mdx
index 8b29390e41a7c0..4a82deefc8d425 100644
--- a/docs/pages/versions/v57.0.0/sdk/ui/universal/row.mdx
+++ b/docs/pages/versions/v57.0.0/sdk/ui/universal/row.mdx
@@ -8,9 +8,26 @@ platforms: ['android', 'ios', 'web', 'expo-go']
import APISection from '~/components/plugins/APISection';
import { APIInstallSection } from '~/components/plugins/InstallSection';
+import { ComponentExample } from '~/ui/components/ComponentExample';
+import { PlatformSpotlight, PlatformTabsGroup } from '~/ui/components/PlatformTabs';
A horizontal layout container that arranges its children from start to end. Delegates to Jetpack Compose's [`Row`](../jetpack-compose/row) on Android, SwiftUI's [`HStack`](../swift-ui/hstack) on iOS, and a flex `View` on web.
+
+
+
+
## Installation
@@ -19,61 +36,118 @@ A horizontal layout container that arranges its children from start to end. Dele
### Basic row
-```tsx RowExample.tsx
+
+
+```tsx
+import { useColorScheme } from 'react-native';
import { Host, Row, Text } from '@expo/ui';
export default function RowExample() {
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
+
return (
- One
- Two
- Three
+ One
+ Two
+ Three
);
}
```
+
+
### Alignment
Use [`alignment`](#alignment) to position children along the cross (vertical) axis.
-```tsx RowAlignmentExample.tsx
+
+
+```tsx
+import { useColorScheme } from 'react-native';
import { Host, Row, Text } from '@expo/ui';
export default function RowAlignmentExample() {
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
+
return (
-
+
- Centered
- Centered
+ Large
+ centered
);
}
```
+
+
### Pushing content apart with Spacer
Pair `Row` with a flexible [`Spacer`](spacer) to push its children to the opposite ends.
-```tsx RowSpacerExample.tsx
+
+
+```tsx
+import { useColorScheme } from 'react-native';
import { Host, Row, Text, Spacer } from '@expo/ui';
export default function RowSpacerExample() {
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
+
return (
-
+
- Leading
+ Leading
- Trailing
+ Trailing
);
}
```
+
+
## API
```tsx
@@ -81,3 +155,5 @@ import { Row } from '@expo/ui';
```
+
+
diff --git a/docs/pages/versions/v57.0.0/sdk/ui/universal/scrollview.mdx b/docs/pages/versions/v57.0.0/sdk/ui/universal/scrollview.mdx
index 61cc02f9bf7dd5..0b649bb40258ea 100644
--- a/docs/pages/versions/v57.0.0/sdk/ui/universal/scrollview.mdx
+++ b/docs/pages/versions/v57.0.0/sdk/ui/universal/scrollview.mdx
@@ -8,9 +8,26 @@ platforms: ['android', 'ios', 'web', 'expo-go']
import APISection from '~/components/plugins/APISection';
import { APIInstallSection } from '~/components/plugins/InstallSection';
+import { ComponentExample } from '~/ui/components/ComponentExample';
+import { PlatformSpotlight, PlatformTabsGroup } from '~/ui/components/PlatformTabs';
A scrollable container, defaulting to vertical scrolling. Use [`direction="horizontal"`](#direction) for horizontal lists.
+
+
+
+
## Installation
@@ -19,16 +36,35 @@ A scrollable container, defaulting to vertical scrolling. Use [`direction="horiz
### Vertical scrolling
-```tsx VerticalScrollViewExample.tsx
+
+
+```tsx
+import { useColorScheme } from 'react-native';
import { Host, ScrollView, Column, Text } from '@expo/ui';
export default function VerticalScrollViewExample() {
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
+
return (
{Array.from({ length: 30 }).map((_, i) => (
- Row {i + 1}
+
+ {`Row ${i + 1}`}
+
))}
@@ -37,18 +73,39 @@ export default function VerticalScrollViewExample() {
}
```
+
+
### Horizontal scrolling
-```tsx HorizontalScrollViewExample.tsx
+
+
+```tsx
+import { useColorScheme } from 'react-native';
import { Host, ScrollView, Row, Text } from '@expo/ui';
export default function HorizontalScrollViewExample() {
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
+
return (
-
+
{Array.from({ length: 20 }).map((_, i) => (
- Item {i + 1}
+
+ {`Item ${i + 1}`}
+
))}
@@ -57,6 +114,8 @@ export default function HorizontalScrollViewExample() {
}
```
+
+
## API
```tsx
@@ -64,3 +123,5 @@ import { ScrollView } from '@expo/ui';
```
+
+
diff --git a/docs/pages/versions/v57.0.0/sdk/ui/universal/slider.mdx b/docs/pages/versions/v57.0.0/sdk/ui/universal/slider.mdx
index 1fdeb153d36b76..5469235bf4c9b1 100644
--- a/docs/pages/versions/v57.0.0/sdk/ui/universal/slider.mdx
+++ b/docs/pages/versions/v57.0.0/sdk/ui/universal/slider.mdx
@@ -8,9 +8,26 @@ platforms: ['android', 'ios', 'web', 'expo-go']
import APISection from '~/components/plugins/APISection';
import { APIInstallSection } from '~/components/plugins/InstallSection';
+import { ComponentExample } from '~/ui/components/ComponentExample';
+import { PlatformSpotlight, PlatformTabsGroup } from '~/ui/components/PlatformTabs';
A controlled slider for selecting a numeric value within a range. Pair [`value`](#value) with [`onValueChange`](#onvaluechange) to manage state from React.
+
+
+
+
## Installation
@@ -21,7 +38,20 @@ A controlled slider for selecting a numeric value within a range. Pair [`value`]
The default range is `[0, 1]`.
-```tsx ContinuousSliderExample.tsx
+
+
+```tsx
import { useState } from 'react';
import { Host, Slider } from '@expo/ui';
@@ -29,28 +59,47 @@ export default function ContinuousSliderExample() {
const [value, setValue] = useState(0.5);
return (
-
+
);
}
```
+
+
### Stepped slider with custom range
Use `min`, `max`, and `step` to constrain the values produced by the slider.
-```tsx SteppedSliderExample.tsx
+
+
+```tsx
import { useState } from 'react';
+import { useColorScheme } from 'react-native';
import { Host, Column, Slider, Text } from '@expo/ui';
export default function SteppedSliderExample() {
const [volume, setVolume] = useState(50);
+ const colorScheme = useColorScheme();
return (
-
+
- Volume: {volume}
+
+ {`Volume: ${volume}`}
+
@@ -58,6 +107,8 @@ export default function SteppedSliderExample() {
}
```
+
+
## API
```tsx
@@ -65,3 +116,5 @@ import { Slider } from '@expo/ui';
```
+
+
diff --git a/docs/pages/versions/v57.0.0/sdk/ui/universal/spacer.mdx b/docs/pages/versions/v57.0.0/sdk/ui/universal/spacer.mdx
index 12c0ac9830dbb9..715c1c2302e2aa 100644
--- a/docs/pages/versions/v57.0.0/sdk/ui/universal/spacer.mdx
+++ b/docs/pages/versions/v57.0.0/sdk/ui/universal/spacer.mdx
@@ -8,9 +8,26 @@ platforms: ['android', 'ios', 'web', 'expo-go']
import APISection from '~/components/plugins/APISection';
import { APIInstallSection } from '~/components/plugins/InstallSection';
+import { ComponentExample } from '~/ui/components/ComponentExample';
+import { PlatformSpotlight, PlatformTabsGroup } from '~/ui/components/PlatformTabs';
A layout spacer that produces empty space between siblings inside a [`Row`](row) or [`Column`](column). Use [`size`](#size) for a fixed gap, or [`flexible`](#flexible) to fill the remaining main-axis space.
+
+
+
+
## Installation
@@ -19,42 +36,80 @@ A layout spacer that produces empty space between siblings inside a [`Row`](row)
### Fixed-size spacer
-```tsx FixedSpacerExample.tsx
+
+
+```tsx
+import { useColorScheme } from 'react-native';
import { Host, Column, Text, Spacer } from '@expo/ui';
export default function FixedSpacerExample() {
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
+
return (
- Top
+ Top
- Bottom
+ Bottom
);
}
```
+
+
### Flexible spacer
A flexible spacer fills the remaining space along its parent's main axis, pushing the surrounding content to opposite ends.
-```tsx FlexibleSpacerExample.tsx
+
+
+```tsx
+import { useColorScheme } from 'react-native';
import { Host, Row, Text, Spacer } from '@expo/ui';
export default function FlexibleSpacerExample() {
+ const colorScheme = useColorScheme();
+ const ink = { color: colorScheme === 'dark' ? '#FFFFFF' : '#000000' };
+
return (
-
+
- Leading
+ Leading
- Trailing
+ Trailing
);
}
```
+
+
## API
```tsx
@@ -62,3 +117,5 @@ import { Spacer } from '@expo/ui';
```
+
+
diff --git a/docs/pages/versions/v57.0.0/sdk/ui/universal/switch.mdx b/docs/pages/versions/v57.0.0/sdk/ui/universal/switch.mdx
index 04b9866eb76a9d..d3c406385b653e 100644
--- a/docs/pages/versions/v57.0.0/sdk/ui/universal/switch.mdx
+++ b/docs/pages/versions/v57.0.0/sdk/ui/universal/switch.mdx
@@ -8,9 +8,26 @@ platforms: ['android', 'ios', 'web', 'expo-go']
import APISection from '~/components/plugins/APISection';
import { APIInstallSection } from '~/components/plugins/InstallSection';
+import { ComponentExample } from '~/ui/components/ComponentExample';
+import { PlatformSpotlight, PlatformTabsGroup } from '~/ui/components/PlatformTabs';
A controlled toggle. Pair [`value`](#value) with [`onValueChange`](#onvaluechange) to manage state from React.
+
+
+
+
## Installation
@@ -19,7 +36,20 @@ A controlled toggle. Pair [`value`](#value) with [`onValueChange`](#onvaluechang
### Basic switch
-```tsx SwitchExample.tsx
+
+
+```tsx
import { useState } from 'react';
import { Host, Switch } from '@expo/ui';
@@ -34,11 +64,26 @@ export default function SwitchExample() {
}
```
+
+
### With label
When `label` is provided, the switch is rendered alongside its text in a labeled row.
-```tsx LabeledSwitchExample.tsx
+
+
+```tsx
import { useState } from 'react';
import { Host, Switch } from '@expo/ui';
@@ -46,13 +91,15 @@ export default function LabeledSwitchExample() {
const [notifications, setNotifications] = useState(true);
return (
-
+
);
}
```
+
+
## API
```tsx
@@ -60,3 +107,5 @@ import { Switch } from '@expo/ui';
```
+
+
diff --git a/docs/pages/versions/v57.0.0/sdk/ui/universal/text.mdx b/docs/pages/versions/v57.0.0/sdk/ui/universal/text.mdx
index 3a3dfb0840e3dc..374423f4874768 100644
--- a/docs/pages/versions/v57.0.0/sdk/ui/universal/text.mdx
+++ b/docs/pages/versions/v57.0.0/sdk/ui/universal/text.mdx
@@ -8,9 +8,26 @@ platforms: ['android', 'ios', 'web', 'expo-go']
import APISection from '~/components/plugins/APISection';
import { APIInstallSection } from '~/components/plugins/InstallSection';
+import { ComponentExample } from '~/ui/components/ComponentExample';
+import { PlatformSpotlight, PlatformTabsGroup } from '~/ui/components/PlatformTabs';
A component for displaying text. Adapts to the platform color scheme (light/dark) by default and exposes a focused subset of typography knobs through [`textStyle`](#textstyle).
+
+
+
+
## Installation
@@ -19,45 +36,107 @@ A component for displaying text. Adapts to the platform color scheme (light/dark
### Basic text
-```tsx TextExample.tsx
+
+
+```tsx
+import { useColorScheme } from 'react-native';
import { Host, Text } from '@expo/ui';
export default function TextExample() {
+ const colorScheme = useColorScheme();
+
return (
- Hello, world!
+
+ Hello, world!
+
);
}
```
+
+
### Styled text
Use [`textStyle`](#textstyle) for typography-specific properties (font size, weight, alignment).
-```tsx StyledTextExample.tsx
+
+
+```tsx
+import { useColorScheme } from 'react-native';
import { Host, Text } from '@expo/ui';
export default function StyledTextExample() {
+ const colorScheme = useColorScheme();
+
return (
- Headline
+
+ Headline
+
);
}
```
+
+
### Truncating long text
Use [`numberOfLines`](#numberoflines) to clamp long text with a trailing ellipsis.
-```tsx TruncatedTextExample.tsx
+
+
+```tsx
+import { useColorScheme } from 'react-native';
import { Host, Text } from '@expo/ui';
export default function TruncatedTextExample() {
+ const colorScheme = useColorScheme();
+
return (
-
-
+
+
A very long line of text that will be truncated when it does not fit on a single line.
@@ -65,6 +144,8 @@ export default function TruncatedTextExample() {
}
```
+
+
## API
```tsx
@@ -72,3 +153,5 @@ import { Text } from '@expo/ui';
```
+
+
diff --git a/docs/pages/versions/v57.0.0/sdk/ui/universal/textinput.mdx b/docs/pages/versions/v57.0.0/sdk/ui/universal/textinput.mdx
index 98d0c01621015c..6974cedb8c795f 100644
--- a/docs/pages/versions/v57.0.0/sdk/ui/universal/textinput.mdx
+++ b/docs/pages/versions/v57.0.0/sdk/ui/universal/textinput.mdx
@@ -8,11 +8,28 @@ platforms: ['android', 'ios', 'web', 'expo-go']
import APISection from '~/components/plugins/APISection';
import { APIInstallSection } from '~/components/plugins/InstallSection';
+import { ComponentExample } from '~/ui/components/ComponentExample';
+import { PlatformSpotlight, PlatformTabsGroup } from '~/ui/components/PlatformTabs';
A text input that routes to [`TextField`](../jetpack-compose/textfield) from `@expo/ui/jetpack-compose` on Android, [`TextField`](../swift-ui/textfield) from `@expo/ui/swift-ui` on iOS, and React Native's [`TextInput`](https://reactnative.dev/docs/textinput) on web.
The API mirrors React Native's [`TextInput`](https://reactnative.dev/docs/textinput), with two changes: [`value`](#value) and [`selection`](#selection) are observable state objects (created with `useNativeState`), and [`onChangeText`](#onchangetext) can be a worklet for synchronously updating the state on the UI thread.
+
+
+
+
## Installation
@@ -23,7 +40,20 @@ The API mirrors React Native's [`TextInput`](https://reactnative.dev/docs/textin
Omit [`value`](#value) and the field manages its own text internally. Use [`onChangeText`](#onchangetext) to observe edits, and use the [ref](#textinputref) for imperative actions like `focus`, `blur`, and `clear`.
-```tsx UncontrolledTextInputExample.tsx
+
+
+```tsx
import { Button, Column, Host, TextInput, type TextInputRef } from '@expo/ui';
import { useRef } from 'react';
@@ -31,7 +61,7 @@ export default function UncontrolledTextInputExample() {
const inputRef = useRef(null);
return (
-
+
+
### Controlled
Pass [`value`](#value) to drive the field from a `useNativeState` observable. The example below replaces `Hello` with `World` as you type.
-```tsx ControlledTextInputExample.tsx
+
+
+```tsx
import { Host, TextInput, useNativeState } from '@expo/ui';
import { useCallback } from 'react';
export default function ControlledTextInputExample() {
- const text = useNativeState('');
+ const text = useNativeState('Hello');
const handleChangeText = useCallback(
(value: string) => {
@@ -66,20 +111,35 @@ export default function ControlledTextInputExample() {
);
return (
-
+
);
}
```
+
+
### Worklet masking
Add the `'worklet'` directive to [`onChangeText`](#onchangetext) for synchronously updating the state on the UI thread. Writes to `value` land without the JS-thread round-trip that can cause cursor flicker.
> **Note:** Worklets require installing [`react-native-worklets`](https://docs.swmansion.com/react-native-worklets/).
-```tsx PhoneMaskExample.tsx
+
+
+```tsx
import { Host, TextInput, useNativeState } from '@expo/ui';
import { useCallback } from 'react';
@@ -92,7 +152,7 @@ function formatPhone(input: string) {
}
export default function PhoneMaskExample() {
- const phone = useNativeState('');
+ const phone = useNativeState('(555) 123-4567');
const selection = useNativeState({ start: 0, end: 0 });
const handleChangeText = useCallback(
@@ -109,7 +169,7 @@ export default function PhoneMaskExample() {
);
return (
-
+
+
## Unsupported React Native props
Some React Native `TextInput` props are not supported, because Compose's `TextField` or SwiftUI's `TextField` does not expose an equivalent, or because the prop is replaced by a different mechanism. See the [API](#api) section below for the supported props. If a missing prop blocks your use case, [open an issue](https://github.com/expo/expo/issues/new/choose) so it can be prioritized.
@@ -133,3 +195,5 @@ import { TextInput, useNativeState } from '@expo/ui';
```
+
+
diff --git a/docs/public/static/images/expo-ui/checkbox/ios-dark.webp b/docs/public/static/images/expo-ui/checkbox/ios-dark.webp
new file mode 100644
index 00000000000000..d9506790c34476
Binary files /dev/null and b/docs/public/static/images/expo-ui/checkbox/ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/checkbox/ios-light.webp b/docs/public/static/images/expo-ui/checkbox/ios-light.webp
new file mode 100644
index 00000000000000..021fd2039704ff
Binary files /dev/null and b/docs/public/static/images/expo-ui/checkbox/ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/collapsible/android-dark.webp b/docs/public/static/images/expo-ui/collapsible/android-dark.webp
new file mode 100644
index 00000000000000..347c3fff9188cb
Binary files /dev/null and b/docs/public/static/images/expo-ui/collapsible/android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/collapsible/android-light.webp b/docs/public/static/images/expo-ui/collapsible/android-light.webp
new file mode 100644
index 00000000000000..e2189989204414
Binary files /dev/null and b/docs/public/static/images/expo-ui/collapsible/android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/collapsible/ios-dark.webp b/docs/public/static/images/expo-ui/collapsible/ios-dark.webp
new file mode 100644
index 00000000000000..e162ba6325c107
Binary files /dev/null and b/docs/public/static/images/expo-ui/collapsible/ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/collapsible/ios-light.webp b/docs/public/static/images/expo-ui/collapsible/ios-light.webp
new file mode 100644
index 00000000000000..19dce388dcd79c
Binary files /dev/null and b/docs/public/static/images/expo-ui/collapsible/ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/column/ios-dark.webp b/docs/public/static/images/expo-ui/column/ios-dark.webp
new file mode 100644
index 00000000000000..1abb69b796fdf1
Binary files /dev/null and b/docs/public/static/images/expo-ui/column/ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/column/ios-light.webp b/docs/public/static/images/expo-ui/column/ios-light.webp
new file mode 100644
index 00000000000000..535a18f4fa3f3a
Binary files /dev/null and b/docs/public/static/images/expo-ui/column/ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-bottomsheet-basic-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-bottomsheet-basic-android-dark.webp
new file mode 100644
index 00000000000000..2bbe898d13ba26
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-bottomsheet-basic-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-bottomsheet-basic-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-bottomsheet-basic-android-light.webp
new file mode 100644
index 00000000000000..314fa491b2c392
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-bottomsheet-basic-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-bottomsheet-basic-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-bottomsheet-basic-ios-dark.webp
new file mode 100644
index 00000000000000..f64ed2123a3ac1
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-bottomsheet-basic-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-bottomsheet-basic-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-bottomsheet-basic-ios-light.webp
new file mode 100644
index 00000000000000..35e56bae41d41b
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-bottomsheet-basic-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-bottomsheet-no-indicator-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-bottomsheet-no-indicator-android-dark.webp
new file mode 100644
index 00000000000000..c3c0b1cbe31c47
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-bottomsheet-no-indicator-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-bottomsheet-no-indicator-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-bottomsheet-no-indicator-android-light.webp
new file mode 100644
index 00000000000000..d2fcffa132f957
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-bottomsheet-no-indicator-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-bottomsheet-no-indicator-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-bottomsheet-no-indicator-ios-dark.webp
new file mode 100644
index 00000000000000..47cc11b96aeaf2
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-bottomsheet-no-indicator-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-bottomsheet-no-indicator-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-bottomsheet-no-indicator-ios-light.webp
new file mode 100644
index 00000000000000..c90ed31e5f610d
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-bottomsheet-no-indicator-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-bottomsheet-scrollable-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-bottomsheet-scrollable-android-dark.webp
new file mode 100644
index 00000000000000..433a5c653c60ea
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-bottomsheet-scrollable-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-bottomsheet-scrollable-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-bottomsheet-scrollable-android-light.webp
new file mode 100644
index 00000000000000..d52bd41aafb003
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-bottomsheet-scrollable-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-bottomsheet-scrollable-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-bottomsheet-scrollable-ios-dark.webp
new file mode 100644
index 00000000000000..04571b72d22296
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-bottomsheet-scrollable-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-bottomsheet-scrollable-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-bottomsheet-scrollable-ios-light.webp
new file mode 100644
index 00000000000000..7f9f0a9d9772a8
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-bottomsheet-scrollable-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-bottomsheet-snap-points-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-bottomsheet-snap-points-android-dark.webp
new file mode 100644
index 00000000000000..4980021b5944ea
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-bottomsheet-snap-points-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-bottomsheet-snap-points-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-bottomsheet-snap-points-android-light.webp
new file mode 100644
index 00000000000000..7df3961a2a8520
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-bottomsheet-snap-points-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-bottomsheet-snap-points-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-bottomsheet-snap-points-ios-dark.webp
new file mode 100644
index 00000000000000..fcccb148e4d641
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-bottomsheet-snap-points-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-bottomsheet-snap-points-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-bottomsheet-snap-points-ios-light.webp
new file mode 100644
index 00000000000000..56e80854467298
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-bottomsheet-snap-points-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-button-basic-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-button-basic-android-dark.webp
new file mode 100644
index 00000000000000..0b9094a464b2c6
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-button-basic-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-button-basic-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-button-basic-android-light.webp
new file mode 100644
index 00000000000000..0f0538a2fec9cf
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-button-basic-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-button-basic-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-button-basic-ios-dark.webp
new file mode 100644
index 00000000000000..852b4ece007773
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-button-basic-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-button-basic-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-button-basic-ios-light.webp
new file mode 100644
index 00000000000000..d4f94409576acc
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-button-basic-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-button-custom-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-button-custom-android-dark.webp
new file mode 100644
index 00000000000000..830c9a3ee0781c
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-button-custom-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-button-custom-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-button-custom-android-light.webp
new file mode 100644
index 00000000000000..0709ced067b54d
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-button-custom-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-button-custom-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-button-custom-ios-dark.webp
new file mode 100644
index 00000000000000..367d5cb96277ff
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-button-custom-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-button-custom-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-button-custom-ios-light.webp
new file mode 100644
index 00000000000000..d2bc6e161f4b68
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-button-custom-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-button-disabled-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-button-disabled-android-dark.webp
new file mode 100644
index 00000000000000..bc906a8d788db9
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-button-disabled-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-button-disabled-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-button-disabled-android-light.webp
new file mode 100644
index 00000000000000..5a90c1d9cb6a53
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-button-disabled-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-button-disabled-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-button-disabled-ios-dark.webp
new file mode 100644
index 00000000000000..cad6a93c066e8f
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-button-disabled-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-button-disabled-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-button-disabled-ios-light.webp
new file mode 100644
index 00000000000000..8b2ba1ce08dd10
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-button-disabled-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-button-variants-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-button-variants-android-dark.webp
new file mode 100644
index 00000000000000..5fdfe41dc577d7
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-button-variants-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-button-variants-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-button-variants-android-light.webp
new file mode 100644
index 00000000000000..8864b24faa7a48
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-button-variants-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-button-variants-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-button-variants-ios-dark.webp
new file mode 100644
index 00000000000000..234eacd1c1f8b3
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-button-variants-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-button-variants-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-button-variants-ios-light.webp
new file mode 100644
index 00000000000000..a0d08d369ef6a9
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-button-variants-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-checkbox-basic-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-checkbox-basic-android-dark.webp
new file mode 100644
index 00000000000000..68adbee6e76b20
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-checkbox-basic-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-checkbox-basic-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-checkbox-basic-android-light.webp
new file mode 100644
index 00000000000000..d496eaef6ffc81
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-checkbox-basic-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-checkbox-basic-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-checkbox-basic-ios-dark.webp
new file mode 100644
index 00000000000000..8399657ca91337
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-checkbox-basic-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-checkbox-basic-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-checkbox-basic-ios-light.webp
new file mode 100644
index 00000000000000..87c0620956c811
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-checkbox-basic-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-checkbox-disabled-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-checkbox-disabled-android-dark.webp
new file mode 100644
index 00000000000000..cefba815eed321
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-checkbox-disabled-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-checkbox-disabled-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-checkbox-disabled-android-light.webp
new file mode 100644
index 00000000000000..d5b5bb51b5b947
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-checkbox-disabled-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-checkbox-disabled-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-checkbox-disabled-ios-dark.webp
new file mode 100644
index 00000000000000..370a994d0a0432
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-checkbox-disabled-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-checkbox-disabled-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-checkbox-disabled-ios-light.webp
new file mode 100644
index 00000000000000..c33a20cdded4dd
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-checkbox-disabled-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-collapsible-accordion-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-collapsible-accordion-android-dark.webp
new file mode 100644
index 00000000000000..a7f37097836930
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-collapsible-accordion-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-collapsible-accordion-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-collapsible-accordion-android-light.webp
new file mode 100644
index 00000000000000..b3e10c5e5dfba2
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-collapsible-accordion-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-collapsible-accordion-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-collapsible-accordion-ios-dark.webp
new file mode 100644
index 00000000000000..cc7f8f458aca0f
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-collapsible-accordion-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-collapsible-accordion-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-collapsible-accordion-ios-light.webp
new file mode 100644
index 00000000000000..cd85e2b828362a
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-collapsible-accordion-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-collapsible-basic-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-collapsible-basic-android-dark.webp
new file mode 100644
index 00000000000000..f645e02c227b8a
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-collapsible-basic-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-collapsible-basic-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-collapsible-basic-android-light.webp
new file mode 100644
index 00000000000000..f4c0991194f1a4
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-collapsible-basic-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-collapsible-basic-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-collapsible-basic-ios-dark.webp
new file mode 100644
index 00000000000000..159afcd67cc38f
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-collapsible-basic-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-collapsible-basic-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-collapsible-basic-ios-light.webp
new file mode 100644
index 00000000000000..5285bdaf5cc4df
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-collapsible-basic-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-column-alignment-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-column-alignment-android-dark.webp
new file mode 100644
index 00000000000000..06dc58ef70551f
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-column-alignment-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-column-alignment-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-column-alignment-android-light.webp
new file mode 100644
index 00000000000000..9b0bca05f16385
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-column-alignment-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-column-alignment-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-column-alignment-ios-dark.webp
new file mode 100644
index 00000000000000..c07ebf76b29b2a
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-column-alignment-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-column-alignment-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-column-alignment-ios-light.webp
new file mode 100644
index 00000000000000..4d9edacc63e68b
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-column-alignment-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-column-basic-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-column-basic-android-dark.webp
new file mode 100644
index 00000000000000..b988807a07760d
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-column-basic-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-column-basic-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-column-basic-android-light.webp
new file mode 100644
index 00000000000000..1f23b1f534d2fc
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-column-basic-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-column-basic-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-column-basic-ios-dark.webp
new file mode 100644
index 00000000000000..89cf6a902986ce
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-column-basic-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-column-basic-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-column-basic-ios-light.webp
new file mode 100644
index 00000000000000..6ffab4013c7ddb
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-column-basic-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-fieldgroup-basic-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-fieldgroup-basic-android-dark.webp
new file mode 100644
index 00000000000000..530b407824d8eb
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-fieldgroup-basic-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-fieldgroup-basic-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-fieldgroup-basic-android-light.webp
new file mode 100644
index 00000000000000..7904e383f2ecb7
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-fieldgroup-basic-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-fieldgroup-basic-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-fieldgroup-basic-ios-dark.webp
new file mode 100644
index 00000000000000..316e1eae5edd31
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-fieldgroup-basic-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-fieldgroup-basic-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-fieldgroup-basic-ios-light.webp
new file mode 100644
index 00000000000000..659097e2dde8cf
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-fieldgroup-basic-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-fieldgroup-slots-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-fieldgroup-slots-android-dark.webp
new file mode 100644
index 00000000000000..c563d641752422
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-fieldgroup-slots-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-fieldgroup-slots-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-fieldgroup-slots-android-light.webp
new file mode 100644
index 00000000000000..66a7cb10295e33
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-fieldgroup-slots-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-fieldgroup-slots-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-fieldgroup-slots-ios-dark.webp
new file mode 100644
index 00000000000000..e981b7263b09f0
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-fieldgroup-slots-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-fieldgroup-slots-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-fieldgroup-slots-ios-light.webp
new file mode 100644
index 00000000000000..075ae10d86b3e0
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-fieldgroup-slots-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-host-basic-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-host-basic-android-dark.webp
new file mode 100644
index 00000000000000..2090293fb1bb58
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-host-basic-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-host-basic-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-host-basic-android-light.webp
new file mode 100644
index 00000000000000..a030e4c2b26bc4
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-host-basic-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-host-basic-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-host-basic-ios-dark.webp
new file mode 100644
index 00000000000000..9ea90e4c205f64
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-host-basic-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-host-basic-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-host-basic-ios-light.webp
new file mode 100644
index 00000000000000..ce0b4bd067fb0c
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-host-basic-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-host-color-scheme-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-host-color-scheme-android-dark.webp
new file mode 100644
index 00000000000000..7a9ca392bb3a83
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-host-color-scheme-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-host-color-scheme-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-host-color-scheme-android-light.webp
new file mode 100644
index 00000000000000..130615115a2a63
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-host-color-scheme-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-host-color-scheme-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-host-color-scheme-ios-dark.webp
new file mode 100644
index 00000000000000..a49280be93cf28
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-host-color-scheme-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-host-color-scheme-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-host-color-scheme-ios-light.webp
new file mode 100644
index 00000000000000..b88b5244d4ff37
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-host-color-scheme-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-host-ignore-safe-area-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-host-ignore-safe-area-android-dark.webp
new file mode 100644
index 00000000000000..6e065424dd56d4
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-host-ignore-safe-area-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-host-ignore-safe-area-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-host-ignore-safe-area-android-light.webp
new file mode 100644
index 00000000000000..99e96d98543bf5
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-host-ignore-safe-area-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-host-ignore-safe-area-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-host-ignore-safe-area-ios-dark.webp
new file mode 100644
index 00000000000000..e353b26f75edec
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-host-ignore-safe-area-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-host-ignore-safe-area-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-host-ignore-safe-area-ios-light.webp
new file mode 100644
index 00000000000000..cde0a9ea77a0d4
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-host-ignore-safe-area-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-host-layout-direction-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-host-layout-direction-android-dark.webp
new file mode 100644
index 00000000000000..5ba5c04f7113e7
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-host-layout-direction-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-host-layout-direction-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-host-layout-direction-android-light.webp
new file mode 100644
index 00000000000000..cd9ddcdab46574
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-host-layout-direction-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-host-layout-direction-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-host-layout-direction-ios-dark.webp
new file mode 100644
index 00000000000000..60d4beb94de895
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-host-layout-direction-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-host-layout-direction-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-host-layout-direction-ios-light.webp
new file mode 100644
index 00000000000000..45f4261f00e717
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-host-layout-direction-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-host-match-contents-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-host-match-contents-android-dark.webp
new file mode 100644
index 00000000000000..6fe9547eae2fb7
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-host-match-contents-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-host-match-contents-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-host-match-contents-android-light.webp
new file mode 100644
index 00000000000000..cff681d0195dfd
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-host-match-contents-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-host-match-contents-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-host-match-contents-ios-dark.webp
new file mode 100644
index 00000000000000..a60c349f7c24d5
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-host-match-contents-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-host-match-contents-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-host-match-contents-ios-light.webp
new file mode 100644
index 00000000000000..e5a76667006651
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-host-match-contents-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-host-on-layout-content-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-host-on-layout-content-android-dark.webp
new file mode 100644
index 00000000000000..d3d9a135a4527f
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-host-on-layout-content-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-host-on-layout-content-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-host-on-layout-content-android-light.webp
new file mode 100644
index 00000000000000..fd60c5e3df0ce9
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-host-on-layout-content-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-host-on-layout-content-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-host-on-layout-content-ios-dark.webp
new file mode 100644
index 00000000000000..6f763cd65ddff7
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-host-on-layout-content-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-host-on-layout-content-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-host-on-layout-content-ios-light.webp
new file mode 100644
index 00000000000000..2d8eb8e551f6d7
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-host-on-layout-content-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-host-seed-color-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-host-seed-color-android-dark.webp
new file mode 100644
index 00000000000000..88c41d2143439a
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-host-seed-color-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-host-seed-color-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-host-seed-color-android-light.webp
new file mode 100644
index 00000000000000..82f57c3ffbc569
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-host-seed-color-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-host-seed-color-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-host-seed-color-ios-dark.webp
new file mode 100644
index 00000000000000..bbbde5b347dbb0
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-host-seed-color-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-host-seed-color-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-host-seed-color-ios-light.webp
new file mode 100644
index 00000000000000..185946b2377021
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-host-seed-color-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-host-viewport-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-host-viewport-android-dark.webp
new file mode 100644
index 00000000000000..bfd516871082ad
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-host-viewport-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-host-viewport-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-host-viewport-android-light.webp
new file mode 100644
index 00000000000000..7500f9928e9206
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-host-viewport-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-host-viewport-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-host-viewport-ios-dark.webp
new file mode 100644
index 00000000000000..b6afb28cc64023
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-host-viewport-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-host-viewport-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-host-viewport-ios-light.webp
new file mode 100644
index 00000000000000..c34975f31dec96
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-host-viewport-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-icon-hoisted-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-icon-hoisted-android-dark.webp
new file mode 100644
index 00000000000000..e2fe1a54e919fc
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-icon-hoisted-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-icon-hoisted-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-icon-hoisted-android-light.webp
new file mode 100644
index 00000000000000..70690dab5cd590
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-icon-hoisted-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-icon-hoisted-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-icon-hoisted-ios-dark.webp
new file mode 100644
index 00000000000000..3d30be2e74b481
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-icon-hoisted-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-icon-hoisted-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-icon-hoisted-ios-light.webp
new file mode 100644
index 00000000000000..00d61793e50666
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-icon-hoisted-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-icon-select-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-icon-select-android-dark.webp
new file mode 100644
index 00000000000000..da919b8ae9e73c
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-icon-select-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-icon-select-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-icon-select-android-light.webp
new file mode 100644
index 00000000000000..57c8dcadf238bb
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-icon-select-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-icon-select-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-icon-select-ios-dark.webp
new file mode 100644
index 00000000000000..64c9bb57a87fc3
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-icon-select-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-icon-select-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-icon-select-ios-light.webp
new file mode 100644
index 00000000000000..220b27faabc969
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-icon-select-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-index-basic-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-index-basic-android-dark.webp
new file mode 100644
index 00000000000000..2090293fb1bb58
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-index-basic-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-index-basic-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-index-basic-android-light.webp
new file mode 100644
index 00000000000000..a030e4c2b26bc4
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-index-basic-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-index-basic-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-index-basic-ios-dark.webp
new file mode 100644
index 00000000000000..9ea90e4c205f64
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-index-basic-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-index-basic-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-index-basic-ios-light.webp
new file mode 100644
index 00000000000000..ce0b4bd067fb0c
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-index-basic-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-list-basic-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-list-basic-android-dark.webp
new file mode 100644
index 00000000000000..a76d1267fb20f3
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-list-basic-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-list-basic-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-list-basic-android-light.webp
new file mode 100644
index 00000000000000..d86e2af8153466
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-list-basic-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-list-basic-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-list-basic-ios-dark.webp
new file mode 100644
index 00000000000000..edf8e9d5016ea7
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-list-basic-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-list-basic-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-list-basic-ios-light.webp
new file mode 100644
index 00000000000000..9d4ab77d9843ae
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-list-basic-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-list-compound-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-list-compound-android-dark.webp
new file mode 100644
index 00000000000000..e2d839855b994a
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-list-compound-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-list-compound-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-list-compound-android-light.webp
new file mode 100644
index 00000000000000..71031ec3431e58
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-list-compound-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-list-compound-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-list-compound-ios-dark.webp
new file mode 100644
index 00000000000000..2734e7c8596c2b
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-list-compound-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-list-compound-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-list-compound-ios-light.webp
new file mode 100644
index 00000000000000..9dca430a7af0ee
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-list-compound-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-list-refresh-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-list-refresh-android-dark.webp
new file mode 100644
index 00000000000000..fe2ed06e3b0686
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-list-refresh-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-list-refresh-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-list-refresh-android-light.webp
new file mode 100644
index 00000000000000..0087f3b5ffe67e
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-list-refresh-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-list-refresh-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-list-refresh-ios-dark.webp
new file mode 100644
index 00000000000000..1c9508d99d4b30
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-list-refresh-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-list-refresh-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-list-refresh-ios-light.webp
new file mode 100644
index 00000000000000..618bea89c7410f
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-list-refresh-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-list-slots-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-list-slots-android-dark.webp
new file mode 100644
index 00000000000000..1be95d13fc382a
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-list-slots-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-list-slots-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-list-slots-android-light.webp
new file mode 100644
index 00000000000000..946edbe9b3fa2e
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-list-slots-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-list-slots-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-list-slots-ios-dark.webp
new file mode 100644
index 00000000000000..6144f0fdb5cef9
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-list-slots-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-list-slots-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-list-slots-ios-light.webp
new file mode 100644
index 00000000000000..e1e52c6b9ad0f2
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-list-slots-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-picker-menu-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-picker-menu-android-dark.webp
new file mode 100644
index 00000000000000..da1c7092d74617
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-picker-menu-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-picker-menu-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-picker-menu-android-light.webp
new file mode 100644
index 00000000000000..445d94a5ce14c0
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-picker-menu-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-picker-menu-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-picker-menu-ios-dark.webp
new file mode 100644
index 00000000000000..65769746a94d10
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-picker-menu-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-picker-menu-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-picker-menu-ios-light.webp
new file mode 100644
index 00000000000000..54c90e37abe1b5
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-picker-menu-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-picker-wheel-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-picker-wheel-android-dark.webp
new file mode 100644
index 00000000000000..be151c46c8214c
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-picker-wheel-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-picker-wheel-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-picker-wheel-android-light.webp
new file mode 100644
index 00000000000000..52c630bbd55026
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-picker-wheel-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-picker-wheel-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-picker-wheel-ios-dark.webp
new file mode 100644
index 00000000000000..0d84b209d74489
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-picker-wheel-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-picker-wheel-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-picker-wheel-ios-light.webp
new file mode 100644
index 00000000000000..ac4b8c5fb405d2
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-picker-wheel-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-rnhostview-basic-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-rnhostview-basic-android-dark.webp
new file mode 100644
index 00000000000000..90ca21a898e143
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-rnhostview-basic-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-rnhostview-basic-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-rnhostview-basic-android-light.webp
new file mode 100644
index 00000000000000..c916b3b726fa25
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-rnhostview-basic-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-rnhostview-basic-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-rnhostview-basic-ios-dark.webp
new file mode 100644
index 00000000000000..4bbcb9cf9dbe71
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-rnhostview-basic-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-rnhostview-basic-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-rnhostview-basic-ios-light.webp
new file mode 100644
index 00000000000000..9aba2e8849c72d
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-rnhostview-basic-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-rnhostview-sizing-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-rnhostview-sizing-android-dark.webp
new file mode 100644
index 00000000000000..6355e1e8ade611
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-rnhostview-sizing-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-rnhostview-sizing-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-rnhostview-sizing-android-light.webp
new file mode 100644
index 00000000000000..9658c77f3d10df
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-rnhostview-sizing-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-rnhostview-sizing-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-rnhostview-sizing-ios-dark.webp
new file mode 100644
index 00000000000000..2379ad7f75faf1
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-rnhostview-sizing-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-rnhostview-sizing-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-rnhostview-sizing-ios-light.webp
new file mode 100644
index 00000000000000..374bd00ada50cc
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-rnhostview-sizing-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-row-alignment-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-row-alignment-android-dark.webp
new file mode 100644
index 00000000000000..3ca807c5929c28
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-row-alignment-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-row-alignment-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-row-alignment-android-light.webp
new file mode 100644
index 00000000000000..b2e3fc86137140
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-row-alignment-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-row-alignment-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-row-alignment-ios-dark.webp
new file mode 100644
index 00000000000000..df007ab5c59a53
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-row-alignment-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-row-alignment-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-row-alignment-ios-light.webp
new file mode 100644
index 00000000000000..20473de130cd47
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-row-alignment-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-row-basic-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-row-basic-android-dark.webp
new file mode 100644
index 00000000000000..1a1809e36276e8
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-row-basic-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-row-basic-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-row-basic-android-light.webp
new file mode 100644
index 00000000000000..09f44e64fb459a
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-row-basic-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-row-basic-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-row-basic-ios-dark.webp
new file mode 100644
index 00000000000000..eca705e4df582e
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-row-basic-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-row-basic-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-row-basic-ios-light.webp
new file mode 100644
index 00000000000000..91f958709d9f03
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-row-basic-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-row-spacer-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-row-spacer-android-dark.webp
new file mode 100644
index 00000000000000..a5a3bf8286cada
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-row-spacer-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-row-spacer-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-row-spacer-android-light.webp
new file mode 100644
index 00000000000000..1eaf51c737188f
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-row-spacer-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-row-spacer-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-row-spacer-ios-dark.webp
new file mode 100644
index 00000000000000..25814b23d4ddcb
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-row-spacer-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-row-spacer-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-row-spacer-ios-light.webp
new file mode 100644
index 00000000000000..f152ff73a8184f
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-row-spacer-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-scrollview-horizontal-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-scrollview-horizontal-android-dark.webp
new file mode 100644
index 00000000000000..1537abdf2c7c86
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-scrollview-horizontal-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-scrollview-horizontal-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-scrollview-horizontal-android-light.webp
new file mode 100644
index 00000000000000..5961d802a03d28
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-scrollview-horizontal-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-scrollview-horizontal-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-scrollview-horizontal-ios-dark.webp
new file mode 100644
index 00000000000000..baaee3f61b5b15
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-scrollview-horizontal-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-scrollview-horizontal-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-scrollview-horizontal-ios-light.webp
new file mode 100644
index 00000000000000..13fb97fbda47cc
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-scrollview-horizontal-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-scrollview-vertical-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-scrollview-vertical-android-dark.webp
new file mode 100644
index 00000000000000..1d5ff33220ba26
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-scrollview-vertical-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-scrollview-vertical-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-scrollview-vertical-android-light.webp
new file mode 100644
index 00000000000000..d9e7e5c3e26419
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-scrollview-vertical-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-scrollview-vertical-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-scrollview-vertical-ios-dark.webp
new file mode 100644
index 00000000000000..43e6b8bd9c94af
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-scrollview-vertical-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-scrollview-vertical-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-scrollview-vertical-ios-light.webp
new file mode 100644
index 00000000000000..f2d89cb860f2cc
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-scrollview-vertical-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-slider-continuous-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-slider-continuous-android-dark.webp
new file mode 100644
index 00000000000000..8726ca015fdf3d
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-slider-continuous-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-slider-continuous-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-slider-continuous-android-light.webp
new file mode 100644
index 00000000000000..e03c6c9f9c0846
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-slider-continuous-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-slider-continuous-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-slider-continuous-ios-dark.webp
new file mode 100644
index 00000000000000..73f151c4ef87fb
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-slider-continuous-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-slider-continuous-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-slider-continuous-ios-light.webp
new file mode 100644
index 00000000000000..bec8b065f31642
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-slider-continuous-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-slider-stepped-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-slider-stepped-android-dark.webp
new file mode 100644
index 00000000000000..7c0b4fa2e9ce94
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-slider-stepped-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-slider-stepped-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-slider-stepped-android-light.webp
new file mode 100644
index 00000000000000..a69c9b3c3c11b6
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-slider-stepped-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-slider-stepped-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-slider-stepped-ios-dark.webp
new file mode 100644
index 00000000000000..5ed8d172fd4f02
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-slider-stepped-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-slider-stepped-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-slider-stepped-ios-light.webp
new file mode 100644
index 00000000000000..0b562019885a08
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-slider-stepped-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-spacer-fixed-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-spacer-fixed-android-dark.webp
new file mode 100644
index 00000000000000..543e28165253c2
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-spacer-fixed-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-spacer-fixed-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-spacer-fixed-android-light.webp
new file mode 100644
index 00000000000000..8d30f1b0e36ab0
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-spacer-fixed-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-spacer-fixed-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-spacer-fixed-ios-dark.webp
new file mode 100644
index 00000000000000..68cbdb82ff0ab0
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-spacer-fixed-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-spacer-fixed-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-spacer-fixed-ios-light.webp
new file mode 100644
index 00000000000000..f228fa6394bdf4
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-spacer-fixed-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-spacer-flexible-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-spacer-flexible-android-dark.webp
new file mode 100644
index 00000000000000..a5a3bf8286cada
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-spacer-flexible-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-spacer-flexible-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-spacer-flexible-android-light.webp
new file mode 100644
index 00000000000000..1eaf51c737188f
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-spacer-flexible-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-spacer-flexible-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-spacer-flexible-ios-dark.webp
new file mode 100644
index 00000000000000..25814b23d4ddcb
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-spacer-flexible-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-spacer-flexible-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-spacer-flexible-ios-light.webp
new file mode 100644
index 00000000000000..f152ff73a8184f
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-spacer-flexible-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-switch-basic-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-switch-basic-android-dark.webp
new file mode 100644
index 00000000000000..168ac56bf2f777
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-switch-basic-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-switch-basic-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-switch-basic-android-light.webp
new file mode 100644
index 00000000000000..3739cd4cf9ce59
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-switch-basic-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-switch-basic-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-switch-basic-ios-dark.webp
new file mode 100644
index 00000000000000..f5fb39c9787f0b
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-switch-basic-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-switch-basic-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-switch-basic-ios-light.webp
new file mode 100644
index 00000000000000..280f3a0ae48491
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-switch-basic-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-switch-labeled-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-switch-labeled-android-dark.webp
new file mode 100644
index 00000000000000..aef9e22840e760
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-switch-labeled-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-switch-labeled-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-switch-labeled-android-light.webp
new file mode 100644
index 00000000000000..921ebab3839f0a
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-switch-labeled-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-switch-labeled-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-switch-labeled-ios-dark.webp
new file mode 100644
index 00000000000000..471ea40cc43b02
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-switch-labeled-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-switch-labeled-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-switch-labeled-ios-light.webp
new file mode 100644
index 00000000000000..8e095a0c53872c
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-switch-labeled-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-text-basic-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-text-basic-android-dark.webp
new file mode 100644
index 00000000000000..bb166653a18543
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-text-basic-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-text-basic-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-text-basic-android-light.webp
new file mode 100644
index 00000000000000..79cb2fdcf2eeb1
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-text-basic-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-text-basic-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-text-basic-ios-dark.webp
new file mode 100644
index 00000000000000..6f763cd65ddff7
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-text-basic-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-text-basic-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-text-basic-ios-light.webp
new file mode 100644
index 00000000000000..2d8eb8e551f6d7
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-text-basic-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-text-styled-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-text-styled-android-dark.webp
new file mode 100644
index 00000000000000..54564d4c0dc8c2
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-text-styled-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-text-styled-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-text-styled-android-light.webp
new file mode 100644
index 00000000000000..34bc4ed5761c71
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-text-styled-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-text-styled-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-text-styled-ios-dark.webp
new file mode 100644
index 00000000000000..244620ce1765e4
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-text-styled-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-text-styled-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-text-styled-ios-light.webp
new file mode 100644
index 00000000000000..97c65b5436bd12
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-text-styled-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-text-truncated-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-text-truncated-android-dark.webp
new file mode 100644
index 00000000000000..c5e758350c37e6
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-text-truncated-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-text-truncated-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-text-truncated-android-light.webp
new file mode 100644
index 00000000000000..1c7ffc75d1ea94
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-text-truncated-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-text-truncated-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-text-truncated-ios-dark.webp
new file mode 100644
index 00000000000000..40587a46c97b1b
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-text-truncated-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-text-truncated-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-text-truncated-ios-light.webp
new file mode 100644
index 00000000000000..2065cb4de7a364
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-text-truncated-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-textinput-controlled-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-textinput-controlled-android-dark.webp
new file mode 100644
index 00000000000000..804353d987a07e
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-textinput-controlled-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-textinput-controlled-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-textinput-controlled-android-light.webp
new file mode 100644
index 00000000000000..46c7b4157b15db
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-textinput-controlled-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-textinput-controlled-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-textinput-controlled-ios-dark.webp
new file mode 100644
index 00000000000000..8597bbed56dd1a
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-textinput-controlled-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-textinput-controlled-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-textinput-controlled-ios-light.webp
new file mode 100644
index 00000000000000..3cb6263d63a2bd
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-textinput-controlled-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-textinput-mask-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-textinput-mask-android-dark.webp
new file mode 100644
index 00000000000000..1208b323832079
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-textinput-mask-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-textinput-mask-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-textinput-mask-android-light.webp
new file mode 100644
index 00000000000000..4362b28561092d
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-textinput-mask-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-textinput-mask-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-textinput-mask-ios-dark.webp
new file mode 100644
index 00000000000000..2699548332b111
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-textinput-mask-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-textinput-mask-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-textinput-mask-ios-light.webp
new file mode 100644
index 00000000000000..d245268c948c72
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-textinput-mask-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-textinput-uncontrolled-android-dark.webp b/docs/public/static/images/expo-ui/examples/universal-textinput-uncontrolled-android-dark.webp
new file mode 100644
index 00000000000000..d3c7bab5171cea
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-textinput-uncontrolled-android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-textinput-uncontrolled-android-light.webp b/docs/public/static/images/expo-ui/examples/universal-textinput-uncontrolled-android-light.webp
new file mode 100644
index 00000000000000..eaf18033394700
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-textinput-uncontrolled-android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-textinput-uncontrolled-ios-dark.webp b/docs/public/static/images/expo-ui/examples/universal-textinput-uncontrolled-ios-dark.webp
new file mode 100644
index 00000000000000..adb214101f0e1c
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-textinput-uncontrolled-ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/examples/universal-textinput-uncontrolled-ios-light.webp b/docs/public/static/images/expo-ui/examples/universal-textinput-uncontrolled-ios-light.webp
new file mode 100644
index 00000000000000..b3287977429f05
Binary files /dev/null and b/docs/public/static/images/expo-ui/examples/universal-textinput-uncontrolled-ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/fieldgroup/android-dark.webp b/docs/public/static/images/expo-ui/fieldgroup/android-dark.webp
new file mode 100644
index 00000000000000..1effffcbc5acf3
Binary files /dev/null and b/docs/public/static/images/expo-ui/fieldgroup/android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/fieldgroup/android-light.webp b/docs/public/static/images/expo-ui/fieldgroup/android-light.webp
new file mode 100644
index 00000000000000..10487e21b49983
Binary files /dev/null and b/docs/public/static/images/expo-ui/fieldgroup/android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/fieldgroup/ios-dark.webp b/docs/public/static/images/expo-ui/fieldgroup/ios-dark.webp
new file mode 100644
index 00000000000000..934c12f2b8b51c
Binary files /dev/null and b/docs/public/static/images/expo-ui/fieldgroup/ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/fieldgroup/ios-light.webp b/docs/public/static/images/expo-ui/fieldgroup/ios-light.webp
new file mode 100644
index 00000000000000..fc126f75a19edd
Binary files /dev/null and b/docs/public/static/images/expo-ui/fieldgroup/ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/host/android-dark.webp b/docs/public/static/images/expo-ui/host/android-dark.webp
new file mode 100644
index 00000000000000..18417b773178ad
Binary files /dev/null and b/docs/public/static/images/expo-ui/host/android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/host/android-light.webp b/docs/public/static/images/expo-ui/host/android-light.webp
new file mode 100644
index 00000000000000..581bc42f3d38c7
Binary files /dev/null and b/docs/public/static/images/expo-ui/host/android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/host/ios-dark.webp b/docs/public/static/images/expo-ui/host/ios-dark.webp
new file mode 100644
index 00000000000000..0ece4e3f2bd748
Binary files /dev/null and b/docs/public/static/images/expo-ui/host/ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/host/ios-light.webp b/docs/public/static/images/expo-ui/host/ios-light.webp
new file mode 100644
index 00000000000000..b24b6e91f7b1c0
Binary files /dev/null and b/docs/public/static/images/expo-ui/host/ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/icon/ios-dark.webp b/docs/public/static/images/expo-ui/icon/ios-dark.webp
new file mode 100644
index 00000000000000..438549e327d39e
Binary files /dev/null and b/docs/public/static/images/expo-ui/icon/ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/icon/ios-light.webp b/docs/public/static/images/expo-ui/icon/ios-light.webp
new file mode 100644
index 00000000000000..639e7ba1df5fa8
Binary files /dev/null and b/docs/public/static/images/expo-ui/icon/ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/list/android-dark.webp b/docs/public/static/images/expo-ui/list/android-dark.webp
new file mode 100644
index 00000000000000..6562b4d7a2bb52
Binary files /dev/null and b/docs/public/static/images/expo-ui/list/android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/list/android-light.webp b/docs/public/static/images/expo-ui/list/android-light.webp
new file mode 100644
index 00000000000000..859dbafcb8bfe4
Binary files /dev/null and b/docs/public/static/images/expo-ui/list/android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/picker/android-dark.webp b/docs/public/static/images/expo-ui/picker/android-dark.webp
new file mode 100644
index 00000000000000..6cc469d2203c3f
Binary files /dev/null and b/docs/public/static/images/expo-ui/picker/android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/picker/android-light.webp b/docs/public/static/images/expo-ui/picker/android-light.webp
new file mode 100644
index 00000000000000..a211f50c22d14a
Binary files /dev/null and b/docs/public/static/images/expo-ui/picker/android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/rnhostview/android-dark.webp b/docs/public/static/images/expo-ui/rnhostview/android-dark.webp
new file mode 100644
index 00000000000000..39aa5fe0a9b054
Binary files /dev/null and b/docs/public/static/images/expo-ui/rnhostview/android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/rnhostview/android-light.webp b/docs/public/static/images/expo-ui/rnhostview/android-light.webp
new file mode 100644
index 00000000000000..3733538eb07969
Binary files /dev/null and b/docs/public/static/images/expo-ui/rnhostview/android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/rnhostview/ios-dark.webp b/docs/public/static/images/expo-ui/rnhostview/ios-dark.webp
new file mode 100644
index 00000000000000..2a7684651419c0
Binary files /dev/null and b/docs/public/static/images/expo-ui/rnhostview/ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/rnhostview/ios-light.webp b/docs/public/static/images/expo-ui/rnhostview/ios-light.webp
new file mode 100644
index 00000000000000..f1c5fcab4a4697
Binary files /dev/null and b/docs/public/static/images/expo-ui/rnhostview/ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/row/ios-dark.webp b/docs/public/static/images/expo-ui/row/ios-dark.webp
new file mode 100644
index 00000000000000..10972c6f4df011
Binary files /dev/null and b/docs/public/static/images/expo-ui/row/ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/row/ios-light.webp b/docs/public/static/images/expo-ui/row/ios-light.webp
new file mode 100644
index 00000000000000..da2117cb857e69
Binary files /dev/null and b/docs/public/static/images/expo-ui/row/ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/scrollview/android-dark.webp b/docs/public/static/images/expo-ui/scrollview/android-dark.webp
new file mode 100644
index 00000000000000..69995159b76c75
Binary files /dev/null and b/docs/public/static/images/expo-ui/scrollview/android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/scrollview/android-light.webp b/docs/public/static/images/expo-ui/scrollview/android-light.webp
new file mode 100644
index 00000000000000..1120c5a74f0f7b
Binary files /dev/null and b/docs/public/static/images/expo-ui/scrollview/android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/switch/ios-dark.webp b/docs/public/static/images/expo-ui/switch/ios-dark.webp
new file mode 100644
index 00000000000000..8e90e3da0a1471
Binary files /dev/null and b/docs/public/static/images/expo-ui/switch/ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/switch/ios-light.webp b/docs/public/static/images/expo-ui/switch/ios-light.webp
new file mode 100644
index 00000000000000..a8e257738ff673
Binary files /dev/null and b/docs/public/static/images/expo-ui/switch/ios-light.webp differ
diff --git a/docs/public/static/images/expo-ui/textinput/android-dark.webp b/docs/public/static/images/expo-ui/textinput/android-dark.webp
new file mode 100644
index 00000000000000..c7ad57e7abd3f7
Binary files /dev/null and b/docs/public/static/images/expo-ui/textinput/android-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/textinput/android-light.webp b/docs/public/static/images/expo-ui/textinput/android-light.webp
new file mode 100644
index 00000000000000..fc0cf95039150f
Binary files /dev/null and b/docs/public/static/images/expo-ui/textinput/android-light.webp differ
diff --git a/docs/public/static/images/expo-ui/textinput/ios-dark.webp b/docs/public/static/images/expo-ui/textinput/ios-dark.webp
new file mode 100644
index 00000000000000..67ae68c98fb5f4
Binary files /dev/null and b/docs/public/static/images/expo-ui/textinput/ios-dark.webp differ
diff --git a/docs/public/static/images/expo-ui/textinput/ios-light.webp b/docs/public/static/images/expo-ui/textinput/ios-light.webp
new file mode 100644
index 00000000000000..e32f61f4f5d164
Binary files /dev/null and b/docs/public/static/images/expo-ui/textinput/ios-light.webp differ
diff --git a/docs/scenes/versions/unversioned/ui/universal.mdx b/docs/scenes/versions/unversioned/ui/universal.mdx
new file mode 100644
index 00000000000000..b4cacb39bc8bfc
--- /dev/null
+++ b/docs/scenes/versions/unversioned/ui/universal.mdx
@@ -0,0 +1,238 @@
+import { UIComponentCard, UIComponentGrid } from '~/ui/components/UIComponentGrid';
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/scenes/versions/v57.0.0/ui/universal.mdx b/docs/scenes/versions/v57.0.0/ui/universal.mdx
new file mode 100644
index 00000000000000..b4cacb39bc8bfc
--- /dev/null
+++ b/docs/scenes/versions/v57.0.0/ui/universal.mdx
@@ -0,0 +1,238 @@
+import { UIComponentCard, UIComponentGrid } from '~/ui/components/UIComponentGrid';
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/ui/components/ComponentExample.tsx b/docs/ui/components/ComponentExample.tsx
index 89bfd1255f3c0f..1c13d9deac1881 100644
--- a/docs/ui/components/ComponentExample.tsx
+++ b/docs/ui/components/ComponentExample.tsx
@@ -6,6 +6,13 @@ import { cleanCopyValue, getCodeBlockDataFromChildren } from '~/common/code-util
import { prefersDarkTheme } from '~/common/window';
import { usePageApiVersion } from '~/providers/page-api-version';
import { LightboxImage } from '~/ui/components/ContentSpotlight/LightboxImage';
+import { PlatformTabs } from '~/ui/components/PlatformTabs';
+import {
+ type Platform,
+ type PlatformImage,
+ orderPlatforms,
+ usePlatformSelection,
+} from '~/ui/components/PlatformTabs/platform';
import { Snippet } from '~/ui/components/Snippet/Snippet';
import { SnippetContent } from '~/ui/components/Snippet/SnippetContent';
import { SnippetHeader } from '~/ui/components/Snippet/SnippetHeader';
@@ -14,9 +21,11 @@ import { SettingsAction } from '~/ui/components/Snippet/actions/SettingsAction';
type Props = PropsWithChildren<{
title: string;
- src: string;
- alt: string;
+ src?: string;
+ alt?: string;
darkSrc?: string;
+ android?: PlatformImage;
+ ios?: PlatformImage;
}>;
type SideButton = {
@@ -50,6 +59,25 @@ const DEVICE_FRAMES = {
},
};
+function resolveImages({
+ src,
+ darkSrc,
+ alt,
+ android,
+ ios,
+}: Pick): Partial<
+ Record
+> {
+ if (android || ios) {
+ return { ...(android && { android }), ...(ios && { ios }) };
+ }
+ if (!src) {
+ return {};
+ }
+ const platform: Platform = src.includes('-android-') ? 'android' : 'ios';
+ return { [platform]: { src, darkSrc, alt: alt ?? '' } };
+}
+
/**
* Shows a code example next to a screenshot of what it renders, in a single
* snippet card: a full-width header with actions, code on the left, and the
@@ -60,11 +88,15 @@ const DEVICE_FRAMES = {
* rather than a floating image. The platform comes from the file name, which
* every capture carries as `-ios-` or `-android-`, so pages need no extra prop.
*/
-export function ComponentExample({ title, src, darkSrc, alt, children }: Props) {
+export function ComponentExample({ title, src, darkSrc, alt, android, ios, children }: Props) {
const { themeName } = useTheme();
const context = usePageApiVersion();
const [isDark, setDark] = useState(false);
+ const images = resolveImages({ src, darkSrc, alt, android, ios });
+ const available = orderPlatforms(images);
+ const { active, select } = usePlatformSelection(available);
+
useEffect(() => {
if (themeName === 'auto') {
setDark(prefersDarkTheme());
@@ -74,8 +106,9 @@ export function ComponentExample({ title, src, darkSrc, alt, children }: Props)
}, [themeName]);
const { value } = getCodeBlockDataFromChildren(children);
- const activeSrc = isDark && darkSrc ? darkSrc : src;
- const device = src.includes('-android-') ? DEVICE_FRAMES.android : DEVICE_FRAMES.ios;
+ const image = images[active];
+ const activeSrc = image && isDark && image.darkSrc ? image.darkSrc : image?.src;
+ const device = active === 'android' ? DEVICE_FRAMES.android : DEVICE_FRAMES.ios;
return (
@@ -87,36 +120,39 @@ export function ComponentExample({ title, src, darkSrc, alt, children }: Props)
{children}
-