# Satin > Documentation for Satin, a React framework for iPhone and iPad apps that people add to the Home Screen from Safari. Public beta for iOS 27; known issues: https://satin.build/docs/known-issues.html. Canonical page: https://satin.build/docs/components/lazy-stack.html Source: docs/components/lazy-stack.md # LazyVStack and LazyHStack Stable stack layouts for small collections. Both render every child eagerly and retain mounted state. They do not virtualize offscreen content; use [List](https://satin.build/docs/components/list.html) for long data. ```tsx import { LazyVStack, Text } from '@satinhq/kit' {items.map(item => {item.title})} ``` LazyVStack accepts VStack props, including horizontal alignment, spacing, padding and frame. LazyHStack accepts HStack props, including vertical or text-baseline alignment. A frame passes its width to the stack body. Content can take that width with a fill frame or keep an explicit size. These layouts do not scroll by themselves. Put them inside ScrollView when needed. Pinned headers are unsupported. Existing imports from `@satinhq/kit/unstable` remain compatible. --- Canonical page: https://satin.build/docs/components/lazy-grid.html Source: docs/components/lazy-grid.md # LazyVGrid, LazyHGrid and GridItem Arrange a small collection in fixed, flexible or adaptive tracks. These are stable exports from `@satinhq/kit`. All children mount eagerly. There is no virtualization; use [List](https://satin.build/docs/components/list.html) for long data. ```tsx import { GridItem, LazyVGrid, Text, VStack } from '@satinhq/kit' {albums.map(album => ( {album.title} {album.artist} ))} ``` Each cell receives its track width. A fill frame uses it; an explicit width or intrinsic control may stay smaller. `GridItem` alignment places that smaller content within the cell. | Option | Meaning | | --- | --- | | `GridItem.fixed({ size, spacing?, alignment? })` | Fixed track size in points. The older `fixed(size)` call also works. | | `GridItem.flexible({ minimum?, maximum?, spacing?, alignment? })` | Flexible track, default minimum 10 points. | | `GridItem.adaptive({ minimum, maximum?, spacing?, alignment? })` | Repeat one track to fit the available space. | | Track `spacing` | Gap after this column in LazyVGrid or row in LazyHGrid. Default 8. No gap after the last explicit track. | | Track `alignment` | Center, leading, trailing, top, bottom or a corner such as topLeading. Default center. | | `LazyVGrid` | `columns`, horizontal `alignment` and `spacing` between rows. | | `LazyHGrid` | `rows`, vertical `alignment` and `spacing` between columns. | Grid line spacing defaults to 8 points independently of track spacing. Alignment is leading/center/trailing for LazyVGrid and top/center/bottom for LazyHGrid. Use ScrollView when the collection needs scrolling. Mixed adaptive and explicit tracks, pinned headers and virtualization are unsupported. Adaptive tracks require a finite proposal on their track axis. Existing imports from `@satinhq/kit/unstable` remain compatible. --- Canonical page: https://satin.build/docs/web/link.html Source: docs/web/link.md # Link Use `NavigationLink` for typed screen destinations. Use `Link` or `useOpenURL` for a URL. Registered URLs in your app scope use the existing navigation stack and retain the document. An unknown app URL returns `unavailable`; it never turns a route typo into a page reload. ```tsx import { Link, Screen } from '@satinhq/kit' function Example() { return (

Open a page outside this preview.

Visit example.com
) } ``` Call URL actions directly from a press. External HTTP and HTTPS pages open an auxiliary browsing context with its opener removed. The browser chooses its presentation. On the installed target, this can be a page with Safari controls above it; use its Close control to return. `dispatched` means the browser accepted the request, not that the page loaded. There is no reliable generic dismissal callback. The app remains alive while the platform permits it; enable scene restoration for eviction. `prefersInApp` defaults to true. Setting it to false returns `unavailable` because web code cannot guarantee a switch to a separate browser. A blocked auxiliary window also returns `unavailable`. There is no automatic document-replacing fallback. Only HTTP and HTTPS URLs are supported. Executable URLs, embedded credentials, custom schemes and private Safari schemes are rejected. `Link` uses the framework press policy and exposes link accessibility semantics. It is a URL action, not an HTML anchor with a browser context menu. It takes `destination`, `children`, `disabled`, `className`, `style`, `prefersInApp` and `onResult`. --- Canonical page: https://satin.build/docs/web/link-props.html Source: docs/web/link-props.md # LinkProps Configure a URL action with destination, its accessible children and optional disabled state. onResult receives the synchronous dispatch outcome. Internal destinations use the registered in-memory scene stack. A URL action does not expose a generic browser dismissal event. ```ts export interface LinkProps extends OpenURLOptions { destination: string children: ReactNode disabled?: boolean className?: string style?: CSSProperties /** Receives dispatch or rejection immediately, never inferred page completion. */ onResult?: (result: OpenURLResult) => void } ``` See [Web pages and sign in](https://satin.build/docs/learn/web-handoff.html) for complete examples and the hosting contract. The runnable HTTPS example is in `fixtures/web-handoff/`. --- Canonical page: https://satin.build/docs/web/open-url-action.html Source: docs/web/open-url-action.md # OpenURLAction Call the function returned by useOpenURL directly inside a press handler. The action shares Link URL validation and internal route handling. An asynchronous call may lose browser user activation and return popup-blocked. ```ts export type OpenURLAction = (url: string, options?: OpenURLOptions) => OpenURLResult ``` See [Web pages and sign in](https://satin.build/docs/learn/web-handoff.html) for complete examples and the hosting contract. The runnable HTTPS example is in `fixtures/web-handoff/`. --- Canonical page: https://satin.build/docs/web/open-url-options.html Source: docs/web/open-url-options.md # OpenURLOptions prefersInApp defaults to true. It requests browser-owned auxiliary presentation while preserving the app document. false is a strict external-browser preference and returns unavailable because the web cannot guarantee launching another app. ```ts export interface OpenURLOptions { /** Defaults to true. A strict external-browser request is unavailable on this runtime. */ prefersInApp?: boolean } ``` See [Web pages and sign in](https://satin.build/docs/learn/web-handoff.html) for complete examples and the hosting contract. The runnable HTTPS example is in `fixtures/web-handoff/`. --- Canonical page: https://satin.build/docs/web/open-url-result.html Source: docs/web/open-url-result.md # OpenURLResult handled means a registered app destination was handled in memory. dispatched means the browser accepted an external action; it makes no page-load claim. unavailable includes a reason suitable for choosing an explicit alternative. A blocked action never silently replaces the app document. ```ts export type OpenURLResult = | { status: 'handled'; destination: 'app' } | { status: 'dispatched'; destination: 'web' } | { status: 'unavailable' reason: | 'invalid-url' | 'external-browser' | 'unregistered-app-url' | 'navigation-failed' | 'popup-blocked' } ``` See [Web pages and sign in](https://satin.build/docs/learn/web-handoff.html) for complete examples and the hosting contract. The runnable HTTPS example is in `fixtures/web-handoff/`. --- Canonical page: https://satin.build/docs/web/web-authentication.html Source: docs/web/web-authentication.md # WebAuthentication prepare registers durable PKCE material before a user press. complete runs in the dedicated callback document. resume reconciles stored completion and invokes the app exchange. cancel aborts this instance and persists cancellation. getSnapshot/subscribe expose state; connect adds lifecycle and popup monitoring and returns cleanup. The React hook manages those subscriptions for you. ```ts export interface WebAuthentication { /** Generate PKCE and durably register the transaction before enabling the sign-in button. */ prepare(): Promise /** Validate and store the callback on the dedicated callback document. */ complete(): Promise /** Reconcile durable completion on launch, pageshow, focus and return from suspension. */ resume(): Promise cancel(): void getSnapshot(): WebAuthenticationState subscribe(listener: () => void): () => void /** Attach bounded popup monitoring and lifecycle listeners. Returns their cleanup. */ connect(): () => void } ``` See [Web pages and sign in](https://satin.build/docs/learn/web-handoff.html) for complete examples and the hosting contract. The runnable HTTPS example is in `fixtures/web-handoff/`. --- Canonical page: https://satin.build/docs/web/web-authentication-callback-result.html Source: docs/web/web-authentication-callback-result.md # WebAuthenticationCallbackResult completed means the callback was validated and durably stored for the originating app; it is not session success. Offer returnURL as an explicit Return to app link. rejected indicates a malformed, expired, mismatched or replayed callback; unavailable means callback persistence or its runtime capability was unavailable. Do not show tokens or response parameters in the callback UI. ```ts export type WebAuthenticationCallbackResult = | { status: 'completed'; returnURL: string } | { status: 'rejected' | 'unavailable'; reason: string } ``` See [Web pages and sign in](https://satin.build/docs/learn/web-handoff.html) for complete examples and the hosting contract. The runnable HTTPS example is in `fixtures/web-handoff/`. --- Canonical page: https://satin.build/docs/web/web-authentication-configuration.html Source: docs/web/web-authentication-configuration.md # WebAuthenticationConfiguration Declare one stable configuration per provider. id is unique within the app origin. callbackURL and bridgeURL must be same-origin dedicated endpoints outside the trailing-slash appURL scope. Use HTTPS except loopback development. An OpenID Connect scope requires issuer; your exchange server must verify the token issuer, audience, signature and nonce. timeoutMs defaults to ten minutes. A bridge is optional but needed when callback storage is partitioned. ```ts export interface WebAuthenticationConfiguration { /** Stable name, unique within this app origin. */ id: string authorizationURL: string clientID: string /** Dedicated same-origin callback document outside appURL's scope. Never cache it. */ callbackURL: string /** The app's trailing-slash scope URL. */ appURL: string scopes?: readonly string[] /** Required for OpenID Connect; the server must verify issuer, audience, signature and nonce. */ issuer?: string timeoutMs?: number /** Optional same-origin POST endpoint implementing the documented bridge protocol. */ bridgeURL?: string /** Establish your session on the server. Never return or message tokens to another window. */ exchange: (request: WebAuthenticationExchange) => Promise } ``` See [Web pages and sign in](https://satin.build/docs/learn/web-handoff.html) for complete examples and the hosting contract. The runnable HTTPS example is in `fixtures/web-handoff/`. --- Canonical page: https://satin.build/docs/web/web-authentication-exchange.html Source: docs/web/web-authentication-exchange.md # WebAuthenticationExchange The exchange function receives the authorization code, PKCE verifier, exact callbackURL, transactionID, nonce, optional expectedIssuer and an AbortSignal. Send these to your server over HTTPS. Use transactionID as an idempotency key, validate the provider response and establish a secure server session. Resolve only after session establishment. Never send tokens through window messaging. ```ts export interface WebAuthenticationExchange { code: string codeVerifier: string callbackURL: string /** Use this as the server's idempotency key. */ transactionID: string nonce: string expectedIssuer?: string signal: AbortSignal } ``` See [Web pages and sign in](https://satin.build/docs/learn/web-handoff.html) for complete examples and the hosting contract. The runnable HTTPS example is in `fixtures/web-handoff/`. --- Canonical page: https://satin.build/docs/web/web-authentication-request.html Source: docs/web/web-authentication-request.md # WebAuthenticationRequest Store the request returned by prepare, then enable your sign-in button. Call start synchronously from that press. Popup is the default transport; redirect explicitly replaces the document. A blocked popup may retry using redirect. A dispatched request is single-use: cancel and prepare a fresh request before changing transport after opener loss. ```ts export interface WebAuthenticationRequest { /** Call synchronously in the user's press handler. A blocked popup may retry as redirect. */ start(options?: { transport?: 'popup' | 'redirect' }): WebAuthenticationStartResult } ``` See [Web pages and sign in](https://satin.build/docs/learn/web-handoff.html) for complete examples and the hosting contract. The runnable HTTPS example is in `fixtures/web-handoff/`. --- Canonical page: https://satin.build/docs/web/web-authentication-start-result.html Source: docs/web/web-authentication-start-result.md # WebAuthenticationStartResult dispatched reports the selected transport. unavailable includes the failure reason, including blocked popups or an expired/already-started request. It does not mean that authentication succeeded. Watch WebAuthenticationState for session completion. ```ts export type WebAuthenticationStartResult = | { status: 'dispatched'; transport: 'popup' | 'redirect' } | { status: 'unavailable'; reason: string } ``` See [Web pages and sign in](https://satin.build/docs/learn/web-handoff.html) for complete examples and the hosting contract. The runnable HTTPS example is in `fixtures/web-handoff/`. --- Canonical page: https://satin.build/docs/web/web-authentication-state.html Source: docs/web/web-authentication-state.md # WebAuthenticationState idle is the initial state. prepared, pending and completing include expiresAt and needsReturn. A closed or COOP-severed popup can set needsReturn without resolving the transaction: closure cannot reliably distinguish cancellation from opener loss. succeeded follows the app exchange; cancelled and expired are explicit terminal states. failed/unavailable carry a reason. Terminal persisted records remove transaction secrets. ```ts export type WebAuthenticationState = | { status: 'idle' } | { status: 'prepared' | 'pending' | 'completing'; expiresAt: number; needsReturn: boolean } | { status: 'succeeded' | 'cancelled' | 'expired' } | { status: 'failed' | 'unavailable'; reason: string } ``` See [Web pages and sign in](https://satin.build/docs/learn/web-handoff.html) for complete examples and the hosting contract. The runnable HTTPS example is in `fixtures/web-handoff/`. --- Canonical page: https://satin.build/docs/web/web-frame-action-result.html Source: docs/web/web-frame-action-result.md # WebFrameActionResult Inspect this return value after reload or stop. dispatched acknowledges the request; unavailable explains not-mounted or cross-origin-or-denied. Neither outcome promises completion. ```ts export type WebFrameActionResult = | { status: 'dispatched' } | { status: 'unavailable'; reason: 'not-mounted' | 'cross-origin-or-denied' } ``` See [Web pages and sign in](https://satin.build/docs/learn/web-handoff.html) for complete examples and the hosting contract. The runnable HTTPS example is in `fixtures/web-handoff/`. --- Canonical page: https://satin.build/docs/web/web-frame-controller.html Source: docs/web/web-frame-controller.md # WebFrameController Create the controller with useWebFrame and pass it to one WebView. state is reactive in the hook owner. reload and stop return dispatched only for an inspectable same-origin frame; an absent or cross-origin frame returns unavailable. There is no script evaluation or fabricated navigation history. ```ts export interface WebFrameController { readonly state: WebFrameState reload(): WebFrameActionResult stop(): WebFrameActionResult } ``` See [Web pages and sign in](https://satin.build/docs/learn/web-handoff.html) for complete examples and the hosting contract. The runnable HTTPS example is in `fixtures/web-handoff/`. --- Canonical page: https://satin.build/docs/web/web-frame-state.html Source: docs/web/web-frame-state.md # WebFrameState idle means no admitted page; loading means a navigation began. loaded means a load event was observed and the same-origin document could be inspected. It does not prove HTTP success or application readiness. unverified combines cross-origin and denied framing because browser APIs cannot reliably distinguish them. unavailable reports invalid-url or an observed load-error. ```ts export type WebFrameState = | { status: 'idle' | 'loading' } | { status: 'loaded'; url: string; title: string } | { status: 'unverified'; reason: 'cross-origin-or-denied' } | { status: 'unavailable'; reason: 'invalid-url' | 'load-error' } ``` See [Web pages and sign in](https://satin.build/docs/learn/web-handoff.html) for complete examples and the hosting contract. The runnable HTTPS example is in `fixtures/web-handoff/`. --- Canonical page: https://satin.build/docs/web/web-view.html Source: docs/web/web-view.md # WebView ```tsx import { Screen, WebView } from '@satinhq/kit' function Example() { return (
) } ``` `WebView` embeds an iframe. Its parent supplies a height. Scripts and form submission are disabled unless `allowsJavaScript` or `allowsForms` is true. Popups, downloads and top navigation remain sandboxed. Same-origin scripted content must be trusted: `allow-scripts` with `allow-same-origin` is not a security boundary against your own origin. `loaded` means a same-origin document is inspectable after its load event. It does not assert an HTTP success status or application readiness. Foreign or denied frames report `unverified` with `cross-origin-or-denied`. A browser does not reliably reveal which happened. Offer an ordinary Link when embedding is unavailable; never proxy around a page's framing restrictions. `useWebFrame` returns a controller with `state`, `reload()` and `stop()`. Both commands return `unavailable` for foreign, denied or unmounted frames. There is no invented cross-origin title, progress, script evaluation or browser-history API. Embedded navigation can participate in browser session history; qualify your content's edge-back, keyboard and scrolling behavior. Embedded storage can be partitioned. Use the authentication helper for sign in. --- Canonical page: https://satin.build/docs/web/web-view-props.html Source: docs/web/web-view-props.md # WebViewProps url accepts HTTP or HTTPS and title supplies the accessible frame name. Set the container height explicitly. The frame uses a sandbox: scripts and forms are disabled unless explicitly allowed. Same-origin content with scripts enabled must be trusted because it is not a security boundary against your own origin. Popups and top-level navigation remain disallowed. ```ts export interface WebViewProps { url: string /** Accessible name for the embedded page. */ title: string controller?: WebFrameController /** Explicitly allow scripts. Same-origin scripted content must be trusted. */ allowsJavaScript?: boolean allowsForms?: boolean className?: string style?: CSSProperties onStateChange?: (state: WebFrameState) => void } ``` See [Web pages and sign in](https://satin.build/docs/learn/web-handoff.html) for complete examples and the hosting contract. The runnable HTTPS example is in `fixtures/web-handoff/`. --- Canonical page: https://satin.build/docs/web/define-web-authentication.html Source: docs/web/define-web-authentication.md # defineWebAuthentication ## Authorization-code sign in Declare one authentication object outside rendering. Generate PKCE before the tap, then dispatch the prepared request synchronously. ```tsx import { defineWebAuthentication, useWebAuthentication, Button } from '@satinhq/kit' import { useState } from 'react' import type { WebAuthenticationRequest } from '@satinhq/kit' const authentication = defineWebAuthentication({ id: 'account', appURL: 'https://app.example/app/', callbackURL: 'https://app.example/auth/callback', authorizationURL: 'https://accounts.example/authorize', clientID: 'public-client-id', scopes: ['openid', 'profile'], issuer: 'https://accounts.example', bridgeURL: 'https://app.example/auth/bridge', exchange: async ({ signal, ...request }) => { const response = await fetch('/auth/exchange', { method: 'POST', credentials: 'same-origin', cache: 'no-store', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(request), signal, }) if (!response.ok) throw new Error('Sign in failed') }, }) function SignIn() { const state = useWebAuthentication(authentication) const [request, setRequest] = useState(null) return <>

{state.status}

} ``` Handle rejected preparation in your app. A real app can prepare when its sign-in view appears and enable Sign in when ready. `start()` defaults to popup. If it reports `popup-blocked`, offer a new user action calling `request.start({ transport: 'redirect' })`. Redirect replaces the app document. The old JavaScript promise cannot receive its completion; `useWebAuthentication` reconciles durable state when the app launches again. A popup closed by the user and an opener severed by browser isolation can look identical. The state remains pending with `needsReturn: true`; check completion, offer Return and Cancel, and let the transaction expire. To choose redirect after an already-started popup, cancel it, prepare a fresh request and start that request as redirect. A fresh request prevents an old callback from consuming the new sign-in attempt. The callback is a dedicated same-origin document outside the app scope. It imports the same declaration, calls `await authentication.complete()`, and shows a Return to app link using the returned `returnURL`. A popup may offer `window.close()` on that callback document. Returning through an ordinary link can open a new app document or Safari tab; the platform does not guarantee automatic reopening of the installed app. The user can return through Home Screen and the result will reconcile there. The callback must have `Cache-Control: no-store`, `Referrer-Policy: no-referrer`, no analytics, and no third-party subresources. Serve it independently of the app platform/install gate. Keep bridge, exchange and IdP routes out of the app-shell cache. The framework worker passes authorization responses containing state plus code/error through to the network. The helper checks exact callback origin/path, state, response shape, expiry and replay. It stores a callback before removing response parameters with the framework URL projection. It sends only a wake-up notice through `postMessage`, with an exact origin; the receiver also checks the popup source. Codes, verifiers and tokens are never messaged. The pending verifier lives in origin-local storage until completion, cancellation or expiration. A terminal result contains no code, verifier, nonce or bridge proof. `exchange` establishes the server session and resolves only after success. Use `transactionID` as an idempotency key: process eviction after exchange but before the local success write can cause a retry. Verify the code, PKCE verifier, callback and configured provider on the server. For OpenID Connect also validate the token signature, issuer, audience, time claims and supplied nonce. The helper supplies `nonce` and `expectedIssuer`; it does not validate identity tokens. An `iss` response parameter is checked when present; server token issuer validation is required even when it is absent. Use distinct callback URLs for providers that do not identify their issuer in the authorization response. Never put a client secret in this bundle. The helper requires secure context, local storage and Web Locks. HTTP loopback is accepted for local development. It cannot complete while the app is suspended; it resumes on launch, pageshow, visibility, focus or a callback wake-up. Storage and network errors are explicit. Cancellation aborts a cooperating exchange and preserves the cancelled local result; it cannot revoke a session already created by your server. ## Optional server bridge Use a bridge when the callback cannot access the initiating app's storage or opener. It is a same-origin HTTPS POST endpoint outside the app scope. All responses are no-store JSON. The request includes `operation`, `id`, `state` and the fields below. | Operation | Request fields | Response | | --- | --- | --- | | register | proofHash, timeoutMs | { accepted: true } | | complete | response: { state, code } or { state, error } | { accepted: true } | | read | proof | null or the stored response | | clear | proof | { accepted: true } | Register stores the SHA-256 base64url hash of a fresh 256-bit private proof, a bounded expiry and app/provider binding. Read and clear must hash and compare that proof. The proof never appears in the provider URL or callback. Knowledge of state alone must not permit reading or claiming a login. Complete must atomically accept only the first response for an unexpired registered state, reject malformed inputs, and never mint a session. The final server exchange still verifies PKCE. Reject cross-origin requests, bound sizes and rates, and expire abandoned transactions. A non-read success must explicitly return `accepted: true`; HTTP 200 alone is insufficient. The fixture in `fixtures/web-handoff` implements this protocol and verifies one-use PKCE codes. It demonstrates OAuth code exchange with PKCE, not OpenID Connect identity-token validation. Its in-memory IdP and bridge are for local testing only; use durable transactional server storage in production. --- Canonical page: https://satin.build/docs/web/use-open-url.html Source: docs/web/use-open-url.md # useOpenURL Use `NavigationLink` for typed screen destinations. Use `Link` or `useOpenURL` for a URL. Registered URLs in your app scope use the existing navigation stack and retain the document. An unknown app URL returns `unavailable`; it never turns a route typo into a page reload. ```tsx import { useState } from 'react' import { Button, Screen, useOpenURL } from '@satinhq/kit' function Example() { const openURL = useOpenURL() const [result, setResult] = useState('No request yet') return (

{result}

) } ``` Call URL actions directly from a press. External HTTP and HTTPS pages open an auxiliary browsing context with its opener removed. The browser chooses its presentation. On the installed target, this can be a page with Safari controls above it; use its Close control to return. `dispatched` means the browser accepted the request, not that the page loaded. There is no reliable generic dismissal callback. The app remains alive while the platform permits it; enable scene restoration for eviction. `prefersInApp` defaults to true. Setting it to false returns `unavailable` because web code cannot guarantee a switch to a separate browser. A blocked auxiliary window also returns `unavailable`. There is no automatic document-replacing fallback. Only HTTP and HTTPS URLs are supported. Executable URLs, embedded credentials, custom schemes and private Safari schemes are rejected. `Link` uses the framework press policy and exposes link accessibility semantics. It is a URL action, not an HTML anchor with a browser context menu. It takes `destination`, `children`, `disabled`, `className`, `style`, `prefersInApp` and `onResult`. --- Canonical page: https://satin.build/docs/web/use-web-authentication.html Source: docs/web/use-web-authentication.md # useWebAuthentication ## Authorization-code sign in Declare one authentication object outside rendering. Generate PKCE before the tap, then dispatch the prepared request synchronously. ```tsx import { defineWebAuthentication, useWebAuthentication, Button } from '@satinhq/kit' import { useState } from 'react' import type { WebAuthenticationRequest } from '@satinhq/kit' const authentication = defineWebAuthentication({ id: 'account', appURL: 'https://app.example/app/', callbackURL: 'https://app.example/auth/callback', authorizationURL: 'https://accounts.example/authorize', clientID: 'public-client-id', scopes: ['openid', 'profile'], issuer: 'https://accounts.example', bridgeURL: 'https://app.example/auth/bridge', exchange: async ({ signal, ...request }) => { const response = await fetch('/auth/exchange', { method: 'POST', credentials: 'same-origin', cache: 'no-store', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(request), signal, }) if (!response.ok) throw new Error('Sign in failed') }, }) function SignIn() { const state = useWebAuthentication(authentication) const [request, setRequest] = useState(null) return <>

{state.status}

} ``` Handle rejected preparation in your app. A real app can prepare when its sign-in view appears and enable Sign in when ready. `start()` defaults to popup. If it reports `popup-blocked`, offer a new user action calling `request.start({ transport: 'redirect' })`. Redirect replaces the app document. The old JavaScript promise cannot receive its completion; `useWebAuthentication` reconciles durable state when the app launches again. A popup closed by the user and an opener severed by browser isolation can look identical. The state remains pending with `needsReturn: true`; check completion, offer Return and Cancel, and let the transaction expire. To choose redirect after an already-started popup, cancel it, prepare a fresh request and start that request as redirect. A fresh request prevents an old callback from consuming the new sign-in attempt. The callback is a dedicated same-origin document outside the app scope. It imports the same declaration, calls `await authentication.complete()`, and shows a Return to app link using the returned `returnURL`. A popup may offer `window.close()` on that callback document. Returning through an ordinary link can open a new app document or Safari tab; the platform does not guarantee automatic reopening of the installed app. The user can return through Home Screen and the result will reconcile there. The callback must have `Cache-Control: no-store`, `Referrer-Policy: no-referrer`, no analytics, and no third-party subresources. Serve it independently of the app platform/install gate. Keep bridge, exchange and IdP routes out of the app-shell cache. The framework worker passes authorization responses containing state plus code/error through to the network. The helper checks exact callback origin/path, state, response shape, expiry and replay. It stores a callback before removing response parameters with the framework URL projection. It sends only a wake-up notice through `postMessage`, with an exact origin; the receiver also checks the popup source. Codes, verifiers and tokens are never messaged. The pending verifier lives in origin-local storage until completion, cancellation or expiration. A terminal result contains no code, verifier, nonce or bridge proof. `exchange` establishes the server session and resolves only after success. Use `transactionID` as an idempotency key: process eviction after exchange but before the local success write can cause a retry. Verify the code, PKCE verifier, callback and configured provider on the server. For OpenID Connect also validate the token signature, issuer, audience, time claims and supplied nonce. The helper supplies `nonce` and `expectedIssuer`; it does not validate identity tokens. An `iss` response parameter is checked when present; server token issuer validation is required even when it is absent. Use distinct callback URLs for providers that do not identify their issuer in the authorization response. Never put a client secret in this bundle. The helper requires secure context, local storage and Web Locks. HTTP loopback is accepted for local development. It cannot complete while the app is suspended; it resumes on launch, pageshow, visibility, focus or a callback wake-up. Storage and network errors are explicit. Cancellation aborts a cooperating exchange and preserves the cancelled local result; it cannot revoke a session already created by your server. ## Optional server bridge Use a bridge when the callback cannot access the initiating app's storage or opener. It is a same-origin HTTPS POST endpoint outside the app scope. All responses are no-store JSON. The request includes `operation`, `id`, `state` and the fields below. | Operation | Request fields | Response | | --- | --- | --- | | register | proofHash, timeoutMs | { accepted: true } | | complete | response: { state, code } or { state, error } | { accepted: true } | | read | proof | null or the stored response | | clear | proof | { accepted: true } | Register stores the SHA-256 base64url hash of a fresh 256-bit private proof, a bounded expiry and app/provider binding. Read and clear must hash and compare that proof. The proof never appears in the provider URL or callback. Knowledge of state alone must not permit reading or claiming a login. Complete must atomically accept only the first response for an unexpired registered state, reject malformed inputs, and never mint a session. The final server exchange still verifies PKCE. Reject cross-origin requests, bound sizes and rates, and expire abandoned transactions. A non-read success must explicitly return `accepted: true`; HTTP 200 alone is insufficient. The fixture in `fixtures/web-handoff` implements this protocol and verifies one-use PKCE codes. It demonstrates OAuth code exchange with PKCE, not OpenID Connect identity-token validation. Its in-memory IdP and bridge are for local testing only; use durable transactional server storage in production. --- Canonical page: https://satin.build/docs/web/use-web-frame.html Source: docs/web/use-web-frame.md # useWebFrame ```tsx import { Button, Screen, WebView, useWebFrame } from '@satinhq/kit' function Example() { const frame = useWebFrame() return (

Frame: {frame.state.status}

) } ``` `WebView` embeds an iframe. Its parent supplies a height. Scripts and form submission are disabled unless `allowsJavaScript` or `allowsForms` is true. Popups, downloads and top navigation remain sandboxed. Same-origin scripted content must be trusted: `allow-scripts` with `allow-same-origin` is not a security boundary against your own origin. `loaded` means a same-origin document is inspectable after its load event. It does not assert an HTTP success status or application readiness. Foreign or denied frames report `unverified` with `cross-origin-or-denied`. A browser does not reliably reveal which happened. Offer an ordinary Link when embedding is unavailable; never proxy around a page's framing restrictions. `useWebFrame` returns a controller with `state`, `reload()` and `stop()`. Both commands return `unavailable` for foreign, denied or unmounted frames. There is no invented cross-origin title, progress, script evaluation or browser-history API. Embedded navigation can participate in browser session history; qualify your content's edge-back, keyboard and scrolling behavior. Embedded storage can be partitioned. Use the authentication helper for sign in. --- Canonical page: https://satin.build/docs/learn/web-handoff.html Source: docs/web-handoff.md # Web pages and sign in Use `NavigationLink` for typed screen destinations. Use `Link` or `useOpenURL` for a URL. Registered URLs in your app scope use the existing navigation stack and retain the document. An unknown app URL returns `unavailable`; it never turns a route typo into a page reload. ```tsx import { Link, Button, useOpenURL } from '@satinhq/kit' function Help() { const openURL = useOpenURL() return <> Help } ``` Call URL actions directly from a press. External HTTP and HTTPS pages open an auxiliary browsing context with its opener removed. The browser chooses its presentation. On the installed target, this can be a page with Safari controls above it; use its Close control to return. `dispatched` means the browser accepted the request, not that the page loaded. There is no reliable generic dismissal callback. The app remains alive while the platform permits it; enable scene restoration for eviction. `prefersInApp` defaults to true. Setting it to false returns `unavailable` because web code cannot guarantee a switch to a separate browser. A blocked auxiliary window also returns `unavailable`. There is no automatic document-replacing fallback. Only HTTP and HTTPS URLs are supported. Executable URLs, embedded credentials, custom schemes and private Safari schemes are rejected. `Link` uses the framework press policy and exposes link accessibility semantics. It is a URL action, not an HTML anchor with a browser context menu. It takes `destination`, `children`, `disabled`, `className`, `style`, `prefersInApp` and `onResult`. ## Embedded pages ```tsx import { WebView, useWebFrame, Button } from '@satinhq/kit' function EmbeddedHelp() { const frame = useWebFrame() return <>

{frame.state.status}

} ``` `WebView` embeds an iframe. Its parent supplies a height. Scripts and form submission are disabled unless `allowsJavaScript` or `allowsForms` is true. Popups, downloads and top navigation remain sandboxed. Same-origin scripted content must be trusted: `allow-scripts` with `allow-same-origin` is not a security boundary against your own origin. `loaded` means a same-origin document is inspectable after its load event. It does not assert an HTTP success status or application readiness. Foreign or denied frames report `unverified` with `cross-origin-or-denied`. A browser does not reliably reveal which happened. Offer an ordinary Link when embedding is unavailable; never proxy around a page's framing restrictions. `useWebFrame` returns a controller with `state`, `reload()` and `stop()`. Both commands return `unavailable` for foreign, denied or unmounted frames. There is no invented cross-origin title, progress, script evaluation or browser-history API. Embedded navigation can participate in browser session history; qualify your content's edge-back, keyboard and scrolling behavior. Embedded storage can be partitioned. Use the authentication helper for sign in. ## Authorization-code sign in Declare one authentication object outside rendering. Generate PKCE before the tap, then dispatch the prepared request synchronously. ```tsx import { defineWebAuthentication, useWebAuthentication, Button } from '@satinhq/kit' import { useState } from 'react' import type { WebAuthenticationRequest } from '@satinhq/kit' const authentication = defineWebAuthentication({ id: 'account', appURL: 'https://app.example/app/', callbackURL: 'https://app.example/auth/callback', authorizationURL: 'https://accounts.example/authorize', clientID: 'public-client-id', scopes: ['openid', 'profile'], issuer: 'https://accounts.example', bridgeURL: 'https://app.example/auth/bridge', exchange: async ({ signal, ...request }) => { const response = await fetch('/auth/exchange', { method: 'POST', credentials: 'same-origin', cache: 'no-store', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(request), signal, }) if (!response.ok) throw new Error('Sign in failed') }, }) function SignIn() { const state = useWebAuthentication(authentication) const [request, setRequest] = useState(null) return <>

{state.status}

} ``` Handle rejected preparation in your app. A real app can prepare when its sign-in view appears and enable Sign in when ready. `start()` defaults to popup. If it reports `popup-blocked`, offer a new user action calling `request.start({ transport: 'redirect' })`. Redirect replaces the app document. The old JavaScript promise cannot receive its completion; `useWebAuthentication` reconciles durable state when the app launches again. A popup closed by the user and an opener severed by browser isolation can look identical. The state remains pending with `needsReturn: true`; check completion, offer Return and Cancel, and let the transaction expire. To choose redirect after an already-started popup, cancel it, prepare a fresh request and start that request as redirect. A fresh request prevents an old callback from consuming the new sign-in attempt. The callback is a dedicated same-origin document outside the app scope. It imports the same declaration, calls `await authentication.complete()`, and shows a Return to app link using the returned `returnURL`. A popup may offer `window.close()` on that callback document. Returning through an ordinary link can open a new app document or Safari tab; the platform does not guarantee automatic reopening of the installed app. The user can return through Home Screen and the result will reconcile there. The callback must have `Cache-Control: no-store`, `Referrer-Policy: no-referrer`, no analytics, and no third-party subresources. Serve it independently of the app platform/install gate. Keep bridge, exchange and IdP routes out of the app-shell cache. The framework worker passes authorization responses containing state plus code/error through to the network. The helper checks exact callback origin/path, state, response shape, expiry and replay. It stores a callback before removing response parameters with the framework URL projection. It sends only a wake-up notice through `postMessage`, with an exact origin; the receiver also checks the popup source. Codes, verifiers and tokens are never messaged. The pending verifier lives in origin-local storage until completion, cancellation or expiration. A terminal result contains no code, verifier, nonce or bridge proof. `exchange` establishes the server session and resolves only after success. Use `transactionID` as an idempotency key: process eviction after exchange but before the local success write can cause a retry. Verify the code, PKCE verifier, callback and configured provider on the server. For OpenID Connect also validate the token signature, issuer, audience, time claims and supplied nonce. The helper supplies `nonce` and `expectedIssuer`; it does not validate identity tokens. An `iss` response parameter is checked when present; server token issuer validation is required even when it is absent. Use distinct callback URLs for providers that do not identify their issuer in the authorization response. Never put a client secret in this bundle. The helper requires secure context, local storage and Web Locks. HTTP loopback is accepted for local development. It cannot complete while the app is suspended; it resumes on launch, pageshow, visibility, focus or a callback wake-up. Storage and network errors are explicit. Cancellation aborts a cooperating exchange and preserves the cancelled local result; it cannot revoke a session already created by your server. ## Optional server bridge Use a bridge when the callback cannot access the initiating app's storage or opener. It is a same-origin HTTPS POST endpoint outside the app scope. All responses are no-store JSON. The request includes `operation`, `id`, `state` and the fields below. | Operation | Request fields | Response | | --- | --- | --- | | register | proofHash, timeoutMs | { accepted: true } | | complete | response: { state, code } or { state, error } | { accepted: true } | | read | proof | null or the stored response | | clear | proof | { accepted: true } | Register stores the SHA-256 base64url hash of a fresh 256-bit private proof, a bounded expiry and app/provider binding. Read and clear must hash and compare that proof. The proof never appears in the provider URL or callback. Knowledge of state alone must not permit reading or claiming a login. Complete must atomically accept only the first response for an unexpired registered state, reject malformed inputs, and never mint a session. The final server exchange still verifies PKCE. Reject cross-origin requests, bound sizes and rates, and expire abandoned transactions. A non-read success must explicitly return `accepted: true`; HTTP 200 alone is insufficient. The fixture in `fixtures/web-handoff` implements this protocol and verifies one-use PKCE codes. It demonstrates OAuth code exchange with PKCE, not OpenID Connect identity-token validation. Its in-memory IdP and bridge are for local testing only; use durable transactional server storage in production. --- Canonical page: https://satin.build/docs/index.html Source: apps/docs/pages/home.md # Satin documentation Satin is a React framework for iPhone and iPad apps that people add to the Home Screen from Safari. It gives you screens and navigation, lists and forms, sheets and menus, local data, and glass rendered by the framework. This is a public beta for iOS 27. [Known issues](https://satin.build/docs/known-issues.html) lists what iOS limits, what we're fixing and what we won't do. ## Your first hour 1. [Quick start](https://satin.build/docs/learn/quick-start.html): create an app and open it on your iPhone. About 10 minutes. 2. [Build a list app](https://satin.build/docs/learn/lists-and-forms.html): a reading list with a field for adding books. About 15 minutes. 3. [Add it to the Home Screen](https://satin.build/docs/learn/installing-and-updates.html): install your app from Safari and open it like any other. About 5 minutes. 4. [Deploy and update](https://satin.build/docs/learn/deploy.html): put it on a static host, check it, and ship a new version. About 15 minutes. ## Find your way - [Components](https://satin.build/docs/components/index.html): one page per component, with a live preview and the API. - [Recipes](https://satin.build/docs/recipes/index.html): longer examples, such as a messaging thread or local data with backups. - [API reference](https://satin.build/docs/api/index.html): every export, generated from the package itself. ## For agents Every page is also plain Markdown at the same path. The index is `/llms.txt`; `/llms-full.txt` has every page in one file. --- Canonical page: https://satin.build/docs/components/list.html Source: docs/components/list.md # List A scrollable collection of rows. ```tsx import { LabeledContent } from '@satinhq/kit' ``` ## Example Read the three books. Each title and author occupy one row. ```tsx import { LabeledContent, List, Screen, Section } from '@satinhq/kit' function Example() { return (
) } ``` ## Group related rows Put rows in a `Section`. Use `header` and `footer` for text that describes the group. ## Choose a list style The default is `insetGrouped`. Set `listStyle` to `plain`, `grouped` or `sidebar` when the list needs a different row layout. ## Restore the scroll position Give each list a stable `id`. Satin uses it to identify that scroll region when you return to a screen. ## API | Prop | Type | Description | | --- | --- | --- | | `id` | `string` | A stable name for scroll restoration. | | `listStyle` | `ListStyle` | Inset grouped by default. | | `selection` | `ID \| ReadonlySet` | `List(selection:)` | Optional controlled selection for ForEach rows. | | `searchable` | `Searchable` | Search configuration owned by the list. | Related: [Quick start](https://satin.build/docs/learn/quick-start.html), [List](https://satin.build/docs/components/list.html), [Screen](https://satin.build/docs/components/screen.html). --- Canonical page: https://satin.build/docs/components/form.html Source: docs/components/form.md # Form A scrollable group of fields and settings. ```tsx import { Form, Toggle, TextField } from '@satinhq/kit' ``` ## Example Click Notifications to change the setting. ```tsx import { useState } from 'react' import { Form, Screen, Section, TextField, Toggle } from '@satinhq/kit' function Example() { const [name, setName] = useState('Alex') const [notifications, setNotifications] = useState(true) return (
) } ``` ## Bind each field Store editable values in React state. Pass the current value and its setter to each control. ## Describe a group Use a Section header for its name and a footer for explanatory text. Form uses grouped rows without extra CSS. ## API | Prop | Type | Description | | --- | --- | --- | | `id` | `string` | A stable scroll region name. | | `children` | `ReactNode` | Sections and controls. | | `searchable` | `Searchable` | Optional search configuration. | Related: [Quick start](https://satin.build/docs/learn/quick-start.html), [List](https://satin.build/docs/components/list.html), [Screen](https://satin.build/docs/components/screen.html). --- Canonical page: https://satin.build/docs/components/section.html Source: docs/components/section.md # Section A group of rows with an optional heading and footer. ```tsx import { Section } from '@satinhq/kit' ``` ## Example Read the example, then change its values in the code. ```tsx import { LabeledContent, List, Screen, Section } from '@satinhq/kit' function Example() { return (
) } ``` ## Name the group Pass plain text or a React node to `header`. The surrounding List or Form supplies its placement. ## Explain the setting Put a short explanation in `footer`. It appears below the last row. ## Good to know - The example places Account above the two rows and the explanatory footer below them. ## API | Prop | Type | Description | | --- | --- | --- | | `header` | `ReactNode` | Content above the rows. | | `footer` | `ReactNode` | Content below the rows. | | `children` | `ReactNode` | Rows in this group. | Related: [Quick start](https://satin.build/docs/learn/quick-start.html), [List](https://satin.build/docs/components/list.html), [Screen](https://satin.build/docs/components/screen.html). --- Canonical page: https://satin.build/docs/components/list-row.html Source: docs/components/list-row.md # ListRow A row wrapper for content that needs row options. ```tsx import { ListRow, Label } from '@satinhq/kit' ``` ## Example Read the example, then change its values in the code. ```tsx import { Label, List, ListRow, Screen, Section } from '@satinhq/kit' function Example() { return (
) } ``` ## Control the separator Use `listRowSeparator="hidden"` for this row. Other rows keep their separator. ## Set row options `listRowInsets` accepts a number or top, leading, bottom and trailing values. Omit it to use the default row layout. ## Content width A custom stack containing text can stay at its intrinsic width. Give the stack an explicit fill frame with `frame={{ maxWidth: 'infinity' }}` when it should fill the row. See [Known issues](https://satin.build/docs/known-issues.html#working-on). Stock intrinsic controls and media keep their own dimensions. A Button label can request the full width with `frame={{ maxWidth: 'infinity' }}`. ## API | Prop | Type | Description | | --- | --- | --- | | `children` | `ReactNode` | Row content. | | `listRowSeparator` | `'visible' \| `listRowSeparator(_:)` | Separator visibility. | | `listRowInsets` | `number \| `listRowInsets(_:)` | Optional content insets. | | `contextMenu` | `readonly Action[]` | Commands shown on a held row. | Related: [Quick start](https://satin.build/docs/learn/quick-start.html), [List](https://satin.build/docs/components/list.html), [Screen](https://satin.build/docs/components/screen.html). --- Canonical page: https://satin.build/docs/components/labeled-content.html Source: docs/components/labeled-content.md # LabeledContent A label and its value on the same row. ```tsx import { LabeledContent } from '@satinhq/kit' ``` ## Example Read the example, then change its values in the code. ```tsx import { LabeledContent, List, Screen, Section } from '@satinhq/kit' function Example() { return (
) } ``` ## Show a value Pass the row title to `label` and its current value to `value`. ## Place it in a list A List or Form supplies the row spacing and separators. LabeledContent also accepts React nodes for its label and value. ## Good to know - The example displays Author and Published as leading labels, with Jane Austen and 1817 as their trailing values. ## API | Prop | Type | Description | | --- | --- | --- | | `label` | `ReactNode` | The leading label. | | `value` | `ReactNode` | The trailing value. | Related: [Quick start](https://satin.build/docs/learn/quick-start.html), [List](https://satin.build/docs/components/list.html), [Screen](https://satin.build/docs/components/screen.html). --- Canonical page: https://satin.build/docs/components/label.html Source: docs/components/label.md # Label Text with a symbol or another icon beside it. ```tsx import { Label } from '@satinhq/kit' ``` ## Example Read the example, then change its values in the code. ```tsx import { Label, List, Screen, Section } from '@satinhq/kit' function Example() { return (
) } ``` ## Choose a symbol Use `symbol` with a name from the symbol catalog. See [SymbolImage](https://satin.build/docs/components/symbol.html). ## Supply another icon Use `icon` for a React node when the icon is not a catalog symbol. Put the title in children. ## Good to know - The example displays Books with a book symbol and Favorites with a star symbol, each beside its text. ## API | Prop | Type | Description | | --- | --- | --- | | `children` | `ReactNode` | The text. | | `symbol` | `SymbolName` | Optional catalog symbol. | | `icon` | `ReactNode` | Optional custom icon. | Related: [Quick start](https://satin.build/docs/learn/quick-start.html), [List](https://satin.build/docs/components/list.html), [Screen](https://satin.build/docs/components/screen.html). --- Canonical page: https://satin.build/docs/components/empty-state.html Source: docs/components/empty-state.md # ContentUnavailableView A message for a screen with no content to show. ```tsx import { ContentUnavailableView, Button } from '@satinhq/kit' ``` ## Example Click New Note to replace the empty screen with a note. ```tsx import { useState } from 'react' import { Button, ContentUnavailableView, List, Screen, Section } from '@satinhq/kit' function Example() { const [created, setCreated] = useState(false) return ( {created ? (
First note
) : ( )}
) } ``` ## Explain the empty screen Use `title` for the message and `description` for the next step. ## Offer an action Put a Button in children if the person can create content or retry a request. ## API | Prop | Type | Description | | --- | --- | --- | | `title` | `string` | The heading. | | `description` | `ReactNode` | Optional explanation. | | `symbol` | `SymbolName` | Optional system symbol; defaults to tray. | | `icon` | `ReactNode` | Optional custom icon, taking precedence over symbol. | | `children` | `ReactNode` | Optional actions. | Related: [Quick start](https://satin.build/docs/learn/quick-start.html), [List](https://satin.build/docs/components/list.html), [Screen](https://satin.build/docs/components/screen.html). --- Canonical page: https://satin.build/docs/components/photos-picker.html Source: docs/components/photos-picker.md # PhotosPicker Selects one photo or video using a real file input. The browser chooses the source menu and picker UI. This does not embed Apple's PhotosUI framework. `selection` is the accepted `File | null`; `onSelectionChange` receives a new File. `matching` accepts `images`, `videos`, or `imagesAndVideos`. `capture="user"` or `"environment"` supplies a browser camera preference, not a camera guarantee. No photo-library permission probe runs. The browser may offer Photo Library, camera or Files. The control does not expose PhotosPickerItem identifiers, ordered multi-selection, persistent library access, or library-specific content filters. File decoding, object URLs, storage and validation belong to the app. ## Example Choose a photo to read its file name. Cancel keeps the current selection. ```tsx import { useState } from 'react' import { PhotosPicker, LabeledContent, List, Screen, Section } from '@satinhq/kit' function Example() { const [photo, setPhoto] = useState(null) const [message, setMessage] = useState('No photo selected') return (
{ setPhoto(file) setMessage(file.name) }} onError={error => setMessage(error.message)} />
) } ``` ## Selection lifetime Cancel leaves the accepted selection unchanged. The input resets after delivery, allowing the same file to be selected again. Observed disabled, covered-screen, selection or mount changes invalidate an outstanding result. Key the picker by draft identity when replacing a draft whose selection has not changed. Browser events carry no chooser session ID, so overlapping choosers cannot be attributed exactly. Already-started decoding or storage is still app-owned. ## Platform limits and evidence Use an HTTPS installed app. Browser capability and permission policy still apply. Embedded docs previews may block these APIs; open the example in its own page when needed. The framework does not replace a system prompt with a simulated picker, share sheet or paste menu. Software regression coverage is separate from installed-device acceptance. Installed selection, cancellation, focus return and touch feedback for this revision are pending; no native visual or motion parity is claimed. Platform reference: [HTML file upload state](https://html.spec.whatwg.org/multipage/input.html#file-upload-state-(type=file)). --- Canonical page: https://satin.build/docs/components/file-importer.html Source: docs/components/file-importer.md # FileImporter Imports file bytes through the system file chooser. `FileImporter` opens the system chooser from a press. It does not expose a presentation flag that would open a chooser outside a user gesture. Pass `selection` as the accepted File array and update it in `onSelectionChange`. `allowedContentTypes` accepts MIME strings or file extensions, for example `['application/pdf', '.txt']`. These are chooser hints, not content validation or Swift uniform type identifiers. `allowsMultipleSelection` defaults to false. Returned Files are snapshots of selected bytes, not persistent filesystem URLs, directory access, or permission to overwrite the originals. WebKit owns the provider UI and may offer sources in addition to Files. ## Example Choose files to read their names. Cancel keeps the current selection. ```tsx import { useState } from 'react' import { FileImporter, LabeledContent, List, Screen, Section } from '@satinhq/kit' function Example() { const [files, setFiles] = useState([]) const [message, setMessage] = useState('No files selected') return (
{ setFiles(selected) setMessage(selected.map(file => file.name).join(', ')) }} onError={error => setMessage(error.message)} />
) } ``` ## Selection lifetime Cancel leaves the accepted selection unchanged. The input resets after delivery, allowing the same file to be selected again. Observed disabled, covered-screen, selection or mount changes invalidate an outstanding result. Key the picker by draft identity when replacing a draft whose selection has not changed. Browser events carry no chooser session ID, so overlapping choosers cannot be attributed exactly. Already-started decoding or storage is still app-owned. ## Platform limits and evidence Use an HTTPS installed app. Browser capability and permission policy still apply. Embedded docs previews may block these APIs; open the example in its own page when needed. The framework does not replace a system prompt with a simulated picker, share sheet or paste menu. Software regression coverage is separate from installed-device acceptance. Installed selection, cancellation, focus return and touch feedback for this revision are pending; no native visual or motion parity is claimed. Platform reference: [HTML file upload state](https://html.spec.whatwg.org/multipage/input.html#file-upload-state-(type=file)). --- Canonical page: https://satin.build/docs/components/share-link.html Source: docs/components/share-link.md # ShareLink Opens the platform share sheet using Web Share. Pass prepared `item` data containing `title`, `text`, `url`, or `files`. For files, prepare real File objects before the press; the framework checks `navigator.canShare({ files })` and never silently drops unsupported attachments. `onCompletion` reports the browser's completed handoff, not delivery to a recipient. `onError` is required. An AbortError can mean dismissal or no suitable target; it is not proof of a user cancellation. Unsupported sharing or files report NotSupportedError. Other browser errors retain their names. The operating system chooses share targets and previews. Structured transfer objects, custom previews and custom activity lists are unsupported. Keep `item` stable while sharing; changing its identity invalidates delivery to the old owner. ## Example Press Share Note to open the system share sheet. ```tsx import { useState } from 'react' import { ShareLink, LabeledContent, List, Screen, Section } from '@satinhq/kit' function Example() { const [message, setMessage] = useState('Ready to share') const [item] = useState(() => ({ text: 'A note shared from my app.' })) return (
setMessage('Share handoff complete')} onError={error => setMessage( error.name === 'AbortError' ? 'Share dismissed or no target available' : error.message, ) } > Share Note
) } ``` ## Action lifetime Repeated presses are disabled while the browser operation is pending. An observed disabled, covered-screen or mount change suppresses its result. Key the control by draft identity when replacing a draft. This does not dismiss a system sheet or revoke work the platform has already accepted. ## Platform limits and evidence Use an HTTPS installed app. Browser capability and permission policy still apply. Embedded docs previews may block these APIs; open the example in its own page when needed. The framework does not replace a system prompt with a simulated picker, share sheet or paste menu. Software regression coverage is separate from installed-device acceptance. Installed selection, cancellation, focus return and touch feedback for this revision are pending; no native visual or motion parity is claimed. Platform reference: [Web Share specification](https://www.w3.org/TR/web-share/). --- Canonical page: https://satin.build/docs/components/paste-button.html Source: docs/components/paste-button.md # PasteButton Reads plain text with the async Clipboard API inside the user's press. `onPaste` receives a string, which can be empty. `onError` is required and receives permission, unsupported-platform or read failures. WebKit may show its own Paste confirmation before returning text. Dismissal and denial cannot be reliably distinguished. The component never reads clipboard contents during render or probes whether a paste is available. It cannot pre-disable itself based on clipboard contents. This version supports plain text only. Structured clipboard objects and images are unsupported. No hidden editable field, synthetic paste, permission polling or clipboard cache bypasses WebKit's prompt. ## Example Copy some text, then press Paste and approve WebKit’s paste prompt if shown. ```tsx import { useState } from 'react' import { PasteButton, LabeledContent, List, Screen, Section } from '@satinhq/kit' function Example() { const [message, setMessage] = useState('No text pasted') return (
setMessage(text || 'Clipboard contains no text')} onError={error => setMessage(error.message)} />
) } ``` ## Action lifetime Repeated presses are disabled while the browser operation is pending. An observed disabled, covered-screen or mount change suppresses its result. Key the control by draft identity when replacing a draft. This does not dismiss a system sheet or revoke work the platform has already accepted. ## Platform limits and evidence Use an HTTPS installed app. Browser capability and permission policy still apply. Embedded docs previews may block these APIs; open the example in its own page when needed. The framework does not replace a system prompt with a simulated picker, share sheet or paste menu. Software regression coverage is separate from installed-device acceptance. Installed selection, cancellation, focus return and touch feedback for this revision are pending; no native visual or motion parity is claimed. Platform reference: [WebKit async clipboard](https://webkit.org/blog/10855/async-clipboard-api/). --- Canonical page: https://satin.build/docs/components/button.html Source: docs/components/button.md # Button A control that runs an action when pressed. ```tsx import { Button, LabeledContent } from '@satinhq/kit' ``` ## Example Click Add Book to increase the count. ```tsx import { useState } from 'react' import { Button, LabeledContent, List, Screen, Section } from '@satinhq/kit' function Example() { const [count, setCount] = useState(0) return (
) } ``` ## Handle a press Pass a function to `onPress`. Change state in that function to update the screen. ## Choose a role or style Use `role="destructive"` for destructive actions. The automatic style adapts to a list or toolbar. Explicit `buttonStyle` values include `plain`, `bordered`, `borderedProminent` and `glass`. ## Use an icon alone Set `iconOnly` and supply `aria-label`. The accessible label names the action. ## API | Prop | Type | Description | | --- | --- | --- | | `onPress` | `() => void` | Called after a press. | | `symbol` | `SymbolName` | Optional icon. | | `role` | `ButtonRole` | An optional semantic role. | | `disabled` | `boolean` | Prevents activation. | | `buttonStyle` | `BuiltInButtonStyle \| `buttonStyle(_:)` | Automatic contextual style by default. | Related: [Quick start](https://satin.build/docs/learn/quick-start.html), [List](https://satin.build/docs/components/list.html), [Screen](https://satin.build/docs/components/screen.html). Button and Pressable keep their intrinsic label width in rows, stacks and grid cells. To fill the available width, give the immediate label a content frame with `maxWidth: 'infinity'`. A row-style automatic Button in List retains the List row allocation. --- Canonical page: https://satin.build/docs/components/toggle.html Source: docs/components/toggle.md # Toggle A control for a Boolean setting. ```tsx import { Toggle } from '@satinhq/kit' ``` ## Example Click Download over Wi-Fi to turn it off and on. ```tsx import { useState } from 'react' import { List, Screen, Section, Toggle } from '@satinhq/kit' function Example() { const [enabled, setEnabled] = useState(true) return (
) } ``` ## Bind the value Pass a Boolean to `isOn` and a setter to `onChange`. ## Place it in a row Inside a List or Form, Toggle renders its label and switch as one row. Keep the label descriptive. ## API | Prop | Type | Description | | --- | --- | --- | | `label` | `string` | The setting name. | | `isOn` | `boolean` | The current value. | | `onChange` | `(isOn: boolean) => void` | Receives the new value. | | `disabled` | `boolean` | Prevents changes. | Related: [Quick start](https://satin.build/docs/learn/quick-start.html), [List](https://satin.build/docs/components/list.html), [Screen](https://satin.build/docs/components/screen.html). --- Canonical page: https://satin.build/docs/components/text-field.html Source: docs/components/text-field.md # TextField A field for editing a line of text. ```tsx import { TextField } from '@satinhq/kit' ``` ## Example Click the Name field and type. Try the system keyboard on a device. ```tsx import { useState } from 'react' import { List, Screen, Section, TextField } from '@satinhq/kit' function Example() { const [text, setText] = useState('') return (
) } ``` ## Bind the text Keep the text in React state. Pass the string to `value` and its setter to `onChange`. ## Name the field Use `label` for its accessible name. A `prompt` supplies placeholder text when the field is empty. ## Choose keyboard and Return behavior Set `kind` to describe the value, such as `name`, `email` or `decimal`. `submitLabel` supplies the Return-key hint; `onSubmit` handles Return in a single-line field. ## API | Prop | Type | Description | | --- | --- | --- | | `label` | `string` | The accessible name. | | `value` | `string` | The current text. | | `onChange` | `(value: string) => void` | Receives edited text. | | `prompt` | `string` | Optional placeholder. | | `kind` | `TextFieldKind` | Keyboard and autofill intent. | | `submitLabel` | `SubmitLabel` | Return-key hint. | | `onSubmit` | `() => void` | Single-line Return action. | Related: [Quick start](https://satin.build/docs/learn/quick-start.html), [List](https://satin.build/docs/components/list.html), [Screen](https://satin.build/docs/components/screen.html). --- Canonical page: https://satin.build/docs/components/secure-field.html Source: docs/components/secure-field.md # SecureField A field that hides password characters. ```tsx import { SecureField } from '@satinhq/kit' ``` ## Example Click the Password field and type. Try the system keyboard on a device. ```tsx import { useState } from 'react' import { List, Screen, Section, SecureField } from '@satinhq/kit' function Example() { const [text, setText] = useState('') return (
) } ``` ## Bind the text Keep the text in React state. Pass the string to `value` and its setter to `onChange`. ## Name the field Use `label` for its accessible name. A `prompt` supplies placeholder text when the field is empty. ## Choose password autofill `kind="current-password"` is the default. Use `kind="new-password"` when creating a password. `submitLabel` supplies the Return-key hint; `onSubmit` handles Return. ## API | Prop | Type | Description | | --- | --- | --- | | `label` | `string` | The accessible name. | | `value` | `string` | The current text. | | `onChange` | `(value: string) => void` | Receives edited text. | | `prompt` | `string` | Optional placeholder. | | `kind` | `'current-password' \| 'new-password'` | Password autofill intent. | | `submitLabel` | `SubmitLabel` | Return-key hint. | | `onSubmit` | `() => void` | Return action. | Related: [Quick start](https://satin.build/docs/learn/quick-start.html), [List](https://satin.build/docs/components/list.html), [Screen](https://satin.build/docs/components/screen.html). --- Canonical page: https://satin.build/docs/components/text-editor.html Source: docs/components/text-editor.md # TextEditor A field for editing multiple lines of text. ```tsx import { TextEditor } from '@satinhq/kit' ``` ## Example Click the Notes field and type. Try the system keyboard on a device. ```tsx import { useState } from 'react' import { List, Screen, Section, TextEditor } from '@satinhq/kit' function Example() { const [text, setText] = useState('') return (
) } ``` ## Bind the text Keep the text in React state. Pass the string to `value` and its setter to `onChange`. ## Name the field Use `label` for its accessible name. A `prompt` supplies placeholder text when the field is empty. ## Bound the editor's growth Use `minLines` and `maxLines` to bound the editor's height. Return inserts a line break; use a separate Button for saving or sending the text. ## API | Prop | Type | Description | | --- | --- | --- | | `label` | `string` | The accessible name. | | `value` | `string` | The current text. | | `onChange` | `(value: string) => void` | Receives edited text. | | `prompt` | `string` | Optional placeholder. | | `minLines` | `number` | Minimum visible lines. | | `maxLines` | `number` | Optional growth limit. | Related: [Quick start](https://satin.build/docs/learn/quick-start.html), [List](https://satin.build/docs/components/list.html), [Screen](https://satin.build/docs/components/screen.html). --- Canonical page: https://satin.build/docs/components/menu.html Source: docs/components/menu.md # Menu A button that opens a list of commands. ```tsx import { Menu, LabeledContent } from '@satinhq/kit' ``` ## Example Click Sort Books, then Author to change the sort order. ```tsx import { useState } from 'react' import { LabeledContent, List, Menu, Screen, Section } from '@satinhq/kit' function Example() { const [sort, setSort] = useState('Title') return (
setSort('Title'), }, { id: 'author', label: 'Author', checked: sort === 'Author', onPress: () => setSort('Author'), }, ]} />
) } ``` ## Declare commands Each action needs a stable `id` and a `label`. Add `onPress` to run it. ## Show the current choice Set `checked` on selection actions. Use `role="destructive"` for destructive commands and set `disabled` on an action to prevent choosing it. `Menu` does not currently accept a top-level `disabled` prop. See [Known issues](https://satin.build/docs/known-issues.html#working-on). ## Group commands An action can contain a `section` or a `submenu` of actions. Keep each command tied to the current content. ## API | Prop | Type | Description | | --- | --- | --- | | `label` | `ReactNode` | The trigger content. | | `actions` | `readonly Action[]` | Commands, groups and submenus. | | `trigger` | `(props: MenuTriggerProps) => ReactNode` | Optional custom trigger. | Related: [Quick start](https://satin.build/docs/learn/quick-start.html), [List](https://satin.build/docs/components/list.html), [Screen](https://satin.build/docs/components/screen.html). --- Canonical page: https://satin.build/docs/components/screen.html Source: docs/components/screen.md # Screen The title and toolbar configuration for the current screen. ```tsx import { LabeledContent } from '@satinhq/kit' ``` ## Example Read the example, then change its values in the code. ```tsx import { LabeledContent, List, Screen, Section } from '@satinhq/kit' function Example() { return (
) } ``` ## Set the title Use `title` to override the route title. `subtitle` appears under it. ## Choose title display Use `titleDisplay` for `large`, `inline`, `inlineLarge` or `none`. The route can set the initial value before the screen renders. ## API | Prop | Type | Description | | --- | --- | --- | | `title` | `string` | Overrides the route title. | | `subtitle` | `string` | Optional subtitle. | | `titleDisplay` | `TitleDisplayMode` | Title presentation. | | `searchable` | `Searchable` | Optional screen search. | Related: [Quick start](https://satin.build/docs/learn/quick-start.html), [List](https://satin.build/docs/components/list.html), [Screen](https://satin.build/docs/components/screen.html). --- Canonical page: https://satin.build/docs/components/toolbar.html Source: docs/components/toolbar.md # Toolbar Actions placed in the navigation bar or bottom bar. ```tsx import { ToolbarItem, ToolbarItemGroup, ToolbarSpacer, Button, LabeledContent } from '@satinhq/kit' ``` ## Example Click Add Stop in the bottom bar, then Reset. ```tsx import { useState } from 'react' import { Button, LabeledContent, List, Screen, Section, ToolbarItem, ToolbarItemGroup, ToolbarSpacer, } from '@satinhq/kit' function Example() { const [stops, setStops] = useState(0) return ( } >
) } ``` ## Place an action Pass toolbar content through `Screen.toolbar`. Use ToolbarItem with `topBarTrailing`, `topBarLeading` or `bottomBar`. Its Button receives the contextual toolbar style. ## Group related actions ToolbarItemGroup puts its children in the same placement. ToolbarSpacer separates groups with fixed or flexible spacing. ## Include search Use `DefaultToolbarItem kind="search"` with a screen searchable configuration to share the bottom bar with search. ## API | Prop | Type | Description | | --- | --- | --- | | `placement` | `ToolbarItemProps[placement]` | Where actions appear. | | `children` | `ReactNode` | Buttons and other controls. | | `sizing` | `'fixed' \| `ToolbarSpacer` | Spacing behavior on ToolbarSpacer. | | `kind` | `'search'` | The search item supplied by the framework. | Related: [Quick start](https://satin.build/docs/learn/quick-start.html), [List](https://satin.build/docs/components/list.html), [Screen](https://satin.build/docs/components/screen.html). --- Canonical page: https://satin.build/docs/components/searchable.html Source: docs/components/searchable.md # Searchable Search configuration for a screen or list. ```tsx import { LabeledContent } from '@satinhq/kit' ``` ## Example Click search and type Kyoto to filter the list. ```tsx import { useState } from 'react' import { LabeledContent, List, Screen, Section } from '@satinhq/kit' function Example() { const [query, setQuery] = useState('') const cities = ['Kyoto', 'Osaka', 'Tokyo'] return (
{cities .filter(city => city.toLowerCase().includes(query.toLowerCase())) .map(city => ( ))}
) } ``` ## Filter the data Searchable reports text changes. Your screen decides which results to show. ## Choose placement Placement follows device idiom automatically. Existing apps gain iPad behavior when upgrading the kit; no opt-in or new props are required. On iPad, the field occupies the trailing navigation toolbar, expands left while keeping its right edge fixed, and uses an in-field clear button. Automatic sidebar search belongs below that column's toolbar. Explicit `toolbar` stays top-right even in a narrow iPad window. `navigationBarDrawer` keeps the column-owned drawer placement. An empty iPad field collapses when it loses focus. Clearing ordinary toolbar search retains focus; submitting a nonempty query keeps it visible while dismissing the keyboard. In a search-role tab, clearing also dismisses search and restores the floating tab bar without changing the selected tab. Suggestions appear in a popover anchored to the field. Drawer scopes remain below the field. The iPhone placement and separate Close control retain their existing behavior. ## Offer completions Use `suggestions` for completions. Pass `scopes` with `selection` and `onSelectionChange` to control the selected scope. If you control `isPresented`, also supply `onPresentedChange`. ## API | Prop | Type | Description | | --- | --- | --- | | `text` | `string` | The query. | | `onTextChange` | `(text: string) => void` | Receives changes. | | `prompt` | `string` | The placeholder. | | `placement` | `SearchFieldPlacement` | Automatic by default. | | `scopes` | `SearchScopes` | A scope selector with `selection` and `onSelectionChange`. | | `isPresented` / `onPresentedChange` | `boolean` / `(presented: boolean) => void` | Supply both when controlling presentation. | Related: [Quick start](https://satin.build/docs/learn/quick-start.html), [List](https://satin.build/docs/components/list.html), [Screen](https://satin.build/docs/components/screen.html). --- Canonical page: https://satin.build/docs/components/picker.html Source: docs/components/picker.md # Picker A control for choosing one option. ```tsx import { Picker } from '@satinhq/kit' ``` ## Example Click Sort and choose Author. ```tsx import { useState } from 'react' import { List, Picker, Screen, Section } from '@satinhq/kit' function Example() { const [sort, setSort] = useState('title') return (
) } ``` ## Bind the selection Each option has a stable `id` and `label`. Store an option id in `selection` and update it in `onSelectionChange`. ## Choose a picker style `pickerStyle` accepts `menu`, `navigationLink`, `segmented` or `inline`. Omit it for the contextual default. ## API | Prop | Type | Description | | --- | --- | --- | | `label` | `string` | The setting name. | | `options` | `readonly Choice[]` | The available choices. | | `selection` | `string` | The selected option id. | | `onSelectionChange` | `(id: string) => void` | Receives the selected id. | | `pickerStyle` | `PickerStyle` | Optional presentation style. | Related: [Quick start](https://satin.build/docs/learn/quick-start.html), [List](https://satin.build/docs/components/list.html), [Screen](https://satin.build/docs/components/screen.html). --- Canonical page: https://satin.build/docs/components/stepper.html Source: docs/components/stepper.md # Stepper A control that increases or decreases a number. ```tsx import { Stepper } from '@satinhq/kit' ``` ## Example Click the plus button to increase the number of copies. ```tsx import { useState } from 'react' import { List, Screen, Section, Stepper } from '@satinhq/kit' function Example() { const [copies, setCopies] = useState(1) return (
) } ``` ## Set the range Use `min` and `max` to bound the value. At a limit, the corresponding button becomes disabled. ## Choose the increment `step` defaults to 1. Use a different positive step for the unit your field needs. ## API | Prop | Type | Description | | --- | --- | --- | | `label` | `string` | The visible label. | | `value` | `number` | The current value. | | `onChange` | `(value: number) => void` | Receives changes. | | `min / max` | `number` | Optional limits. | | `step` | `number` | The increment, 1 by default. | Related: [Quick start](https://satin.build/docs/learn/quick-start.html), [List](https://satin.build/docs/components/list.html), [Screen](https://satin.build/docs/components/screen.html). --- Canonical page: https://satin.build/docs/components/slider.html Source: docs/components/slider.md # Slider A control for choosing a number along a track. ```tsx import { Slider, LabeledContent } from '@satinhq/kit' ``` ## Example Drag the Volume thumb to change the value. ```tsx import { useState } from 'react' import { LabeledContent, List, Screen, Section, Slider } from '@satinhq/kit' function Example() { const [volume, setVolume] = useState(40) return (
) } ``` ## Set the range Use `min` and `max` for the track bounds. The default range is 0 to 1. ## Snap to values Set `step` to snap values to an increment. Use `onEditingChange` when you need to distinguish dragging from the settled value. ## API | Prop | Type | Description | | --- | --- | --- | | `label` | `string` | The accessible name. | | `value` | `number` | The current number. | | `onChange` | `(value: number) => void` | Receives changes. | | `min / max` | `number` | Track limits. | | `step` | `number` | Optional snapping increment. | Related: [Quick start](https://satin.build/docs/learn/quick-start.html), [List](https://satin.build/docs/components/list.html), [Screen](https://satin.build/docs/components/screen.html). --- Canonical page: https://satin.build/docs/components/alert.html Source: docs/components/alert.md # Alert A modal message with actions that need an immediate answer. ```tsx import { Button, LabeledContent, Alert } from '@satinhq/kit' ``` ## Example Click Remove Download, then Remove to confirm. ```tsx import { useState } from 'react' import { Alert, Button, LabeledContent, List, Screen, Section } from '@satinhq/kit' function Example() { const [shown, setShown] = useState(false) const [removed, setRemoved] = useState(false) return (
setRemoved(true), }, { id: 'cancel', label: 'Cancel', role: 'cancel' }, ]} />
) } ``` ## Bind presentation state Keep `isPresented` in React state and pass its setter to `onPresentedChange`. The presentation updates the binding when it closes. ## Finish the task Use a Done button inside a cover. For an alert or dialog, supply actions with labels and optional roles. Presentation state starts closed after an app relaunch. ## API | Prop | Type | Description | | --- | --- | --- | | `isPresented` | `boolean` | Whether it is open. | | `onPresentedChange` | `(presented: boolean) => void` | Receives dismissal changes. | | `title` | `string` | The accessible title. | | `actions` | `readonly ActionItem[]` | Available commands. | Related: [Quick start](https://satin.build/docs/learn/quick-start.html), [List](https://satin.build/docs/components/list.html), [Screen](https://satin.build/docs/components/screen.html). --- Canonical page: https://satin.build/docs/components/confirmation-dialog.html Source: docs/components/confirmation-dialog.md # ConfirmationDialog A set of choices presented for the current task. ```tsx import { Button, LabeledContent, ConfirmationDialog } from '@satinhq/kit' ``` ## Example Click Remove Download, then Remove to confirm. ```tsx import { useState } from 'react' import { Button, ConfirmationDialog, LabeledContent, List, Screen, Section } from '@satinhq/kit' function Example() { const [shown, setShown] = useState(false) const [removed, setRemoved] = useState(false) return (
setRemoved(true), }, { id: 'cancel', label: 'Cancel', role: 'cancel' }, ]} />
) } ``` ## Bind presentation state Keep `isPresented` in React state and pass its setter to `onPresentedChange`. The presentation updates the binding when it closes. ## Finish the task Use a Done button inside a cover. For an alert or dialog, supply actions with labels and optional roles. Presentation state starts closed after an app relaunch. ## API | Prop | Type | Description | | --- | --- | --- | | `isPresented` | `boolean` | Whether it is open. | | `onPresentedChange` | `(presented: boolean) => void` | Receives dismissal changes. | | `title` | `string` | The accessible title. | | `actions` | `readonly ActionItem[]` | Available commands. | Related: [Quick start](https://satin.build/docs/learn/quick-start.html), [List](https://satin.build/docs/components/list.html), [Screen](https://satin.build/docs/components/screen.html). --- Canonical page: https://satin.build/docs/components/full-screen-cover.html Source: docs/components/full-screen-cover.md # FullScreenCover A presentation that covers the whole screen. ```tsx import { Button, LabeledContent, FullScreenCover } from '@satinhq/kit' ``` ## Example Click Open Editor, then Done to close it. ```tsx import { useState } from 'react' import { Button, FullScreenCover, List, NavigationStack, Screen, Section, ToolbarItem, defineApp, defineNavigation, defineRoutes, route, runApp, useDismiss, } from '@satinhq/kit' import { app as appInput } from './app' function Editor() { const dismiss = useDismiss() return ( } >

Your editor content goes here.

) } function Example() { const [shown, setShown] = useState(false) return (
) } const routes = defineRoutes({ example: route(Example, { title: 'FullScreenCover', titleDisplay: 'large', }), editor: route(Editor, { title: 'Editor', titleDisplay: 'inline' }), }) const navigation = defineNavigation(routes, { stack: { root: 'example' } }) declare module '@satinhq/kit' { interface Register { routes: typeof routes navigation: typeof navigation } } runApp({ app: defineApp(appInput), routes, navigation }) ``` ## Bind presentation state Keep `isPresented` in React state and pass its setter to `onPresentedChange`. The presentation updates the binding when it closes. ## Finish the task Use a Done button inside a cover. For an alert or dialog, supply actions with labels and optional roles. Presentation state starts closed after an app relaunch. ## API | Prop | Type | Description | | --- | --- | --- | | `isPresented` | `boolean` | Whether it is open. | | `onPresentedChange` | `(presented: boolean) => void` | Receives dismissal changes. | | `title` | `string` | The accessible title. | | `children` | `ReactNode` | The cover content. | Related: [Quick start](https://satin.build/docs/learn/quick-start.html), [List](https://satin.build/docs/components/list.html), [Screen](https://satin.build/docs/components/screen.html). --- Canonical page: https://satin.build/docs/components/symbol.html Source: docs/components/symbol.md # SymbolImage An icon from the Satin symbol catalog. ```tsx import { SymbolImage, Label } from '@satinhq/kit' ``` ## Example Read the example, then change its values in the code. ```tsx import { Label, List, Screen, Section, SymbolImage } from '@satinhq/kit' function Example() { return (
) } ``` ## Choose a name The `name` prop is typed as SymbolName. See Available symbols at `node_modules/@satinhq/kit/dist/docs/symbols.md` for supported names. ## Name meaningful icons Use `label` on an icon that conveys information by itself. A Label or an icon-only Button can supply the accessible name instead. ## Good to know - The example displays the filled star beside Favorites and the book beside Library. ## API | Prop | Type | Description | | --- | --- | --- | | `name` | `SymbolName` | The catalog name. | | `label` | `string` | An optional accessible name. | | `weight` | `SymbolWeight` | The symbol weight. | | `scale` | `SymbolScale` | The symbol scale. | Related: [Quick start](https://satin.build/docs/learn/quick-start.html), [List](https://satin.build/docs/components/list.html), [Screen](https://satin.build/docs/components/screen.html). --- Canonical page: https://satin.build/docs/components/image.html Source: docs/components/image.md # Image and AsyncImage An image loaded from an asset or URL. ```tsx import { Image, AsyncImage } from '@satinhq/kit' ``` ## Example Read the example, then change its values in the code. ```tsx import { AsyncImage, Image, List, Screen, Section } from '@satinhq/kit' function Example() { return (
Satin preview icon
) } ``` ## Load a local image Use Image with a bundled asset or same-origin URL. Supply `alt` to describe meaningful content. ## Load a remote image AsyncImage loads a URL and can render loading, success and failure phases. For an image under glass, the server must permit CORS pixel access. See [Glass limitations](https://satin.build/docs/glass/limitations.html). ## Size the image box `contentMode="fit"` contains the whole image inside its box. `"fill"` crops to cover that box. Neither mode allocates the box for you. Set both dimensions when the layout needs a fixed box, or supply explicit CSS width and height for a responsive one. `aspectRatio` describes the box's ratio, not the fitting mode. ### Bounded photo viewer This photo panel has a definite height and uses the available content width. `GeometryReader` measures that panel; both `Zoomable` and `Image` receive its dimensions. The image stays centered inside the box regardless of its own ratio. Replace the source and description with your photo. ```tsx import { Divider, GeometryReader, Image, Screen, Text, VStack, Zoomable, semanticColor, } from '@satinhq/kit' export function PhotoViewer() { return ( {({ size }) => ( Sailboats moored in the harbor at sunset )} Harbor at sunset ) } ``` The 280-point panel is an app layout choice. `padding` supplies the default 16-point content inset; `spacing` separates the panel, divider and caption. The background, separator and caption colors follow appearance. `Zoomable` owns pinch and pan inside the panel. For a full-height composition, give the viewer a definite parent height. `GeometryReader` measures the space its parent allocates. Inside a ScrollView, set an explicit height on the scrolling axis. A photo viewer does not need an extra scroller. ## Good to know - The first example loads the same icon through Image and AsyncImage in separate 120-by-120 boxes. The photo viewer recipe sizes both the image and its Zoomable container to the 280-point panel. ## API | Prop | Type | Description | | --- | --- | --- | | `src` on Image | `string \| ImageAsset` | A bundled asset or same-origin source. | | `src` on AsyncImage | `string` | A URL to load. | | `alt` | `string` | A required description. | | `width / height` | `number \| string` | Optional image dimensions. CSS width and height can size the displayed box. | | `contentMode` | `'fit' \| 'fill'` | Contain the whole image or crop to cover its box. Defaults to `fit`. | | `aspectRatio` | `CSSProperties['aspectRatio']` | Optional CSS ratio for the image box. | | `crossOrigin` | `'anonymous' \| 'use-credentials' \| ''` | CORS loading policy. Glass needs readable pixels. | Related: [Quick start](https://satin.build/docs/learn/quick-start.html), [List](https://satin.build/docs/components/list.html), [Screen](https://satin.build/docs/components/screen.html). --- Canonical page: https://satin.build/docs/components/scroll-view.html Source: docs/components/scroll-view.md # ScrollView A named scroll region for content that is not a list. ```tsx import { ScrollView, ScrollTarget } from '@satinhq/kit' ``` ## Example Click Go to chapter in the toolbar to move to the declared target. ```tsx import { useRef } from 'react' import { Button, ContentMargins, Screen, ScrollView, ScrollTarget, ToolbarItem, type ScrollViewHandle, } from '@satinhq/kit' const stops = [ 'Tokyo', 'Shinagawa', 'Shin-Yokohama', 'Odawara', 'Atami', 'Mishima', 'Shin-Fuji', 'Shizuoka', 'Kakegawa', 'Hamamatsu', 'Toyohashi', 'Mikawa-Anjo', 'Nagoya', 'Gifu-Hashima', 'Maibara', ] function Example() { const scroll = useRef(null) return ( } >

Before Kyoto

{stops.map(stop => (

{stop}

))}

Chapter one

The train leaves Kyoto at nine. Keep this note with your tickets.

The return journey leaves Osaka after lunch.

) } ``` ## Name the scroll region Use a stable `id` for the ScrollView. Its position belongs to the current screen. ## Declare a target Wrap normal-flow content in ScrollTarget with a stable `id`. A vertical ScrollView with a ref enables its handle without another positioning prop. You can also use a scrollPosition binding or observe VerticalScrollGeometry. ## Choose the axis ScrollView scrolls vertically by default. Use `axis="horizontal"` for horizontal content. Target positioning, anchors, geometry callbacks and refresh are vertical-only. ## Center narrow content For a narrow vertical stack, use an explicit full-width frame and center the stack within it. Stack alignment arranges the stack's children; it does not make the stack fill the scroll region. ```tsx import { ScrollView, Text, VStack } from '@satinhq/kit' export function TimerSummary() { return ( 24:00 Focus time ) } ``` The frame fills the available width and centers the narrow stack. For a leading-aligned composition, set both frame and stack alignment to `leading`. Use explicit width and alignment on nested timer or metric groups too; filling the outer frame does not set text alignment inside those groups. ## Message appends `defaultScrollAnchor="bottom"` supports insertion motion for a thread that stays pinned to the bottom. This does not provide replacement or keyed `List` reorder motion. See [Messaging scroll](https://satin.build/docs/recipes/messaging-scroll.html) for the growing-composer recipe, stable message identities and remaining motion limitations. ## API | Prop | Type | Description | | --- | --- | --- | | `id` | `string` | A stable region name. | | `axis` | `'vertical' \| `Axis.Set` | The scroll axis. | | `children` | `ReactNode` | Scrollable content. | | `scrollPosition` | `ScrollPositionBinding` | Optional controlled target position. | Related: [Quick start](https://satin.build/docs/learn/quick-start.html), [List](https://satin.build/docs/components/list.html), [Screen](https://satin.build/docs/components/screen.html). --- Canonical page: https://satin.build/docs/components/safe-area-inset.html Source: docs/components/safe-area-inset.md # SafeAreaInset An accessory that reserves space at a safe edge of its content. ```tsx import { SafeAreaInset, Button, LabeledContent } from '@satinhq/kit' ``` ## Example Click Save in the bottom accessory. ```tsx import { useState } from 'react' import { Button, Glass, GlassEffect, LabeledContent, List, SafeAreaInset, Screen, Section, } from '@satinhq/kit' function Example() { const [saved, setSaved] = useState(false) return ( } >
) } ``` ## Reserve an edge Wrap the main content in SafeAreaInset. Pass the accessory to `content` and choose `top` or `bottom`. ## Adjust safe-area layout Use SafeAreaPadding, ContentMargins, SafeAreaBar or IgnoresSafeArea to control content placement. Components consume framework geometry; they do not need viewport-height or raw safe-area CSS. ## API | Prop | Type | Description | | --- | --- | --- | | `edge` | `'top' \| `VerticalEdge` | The accessory edge. | | `content` | `ReactNode` | The accessory. | | `children` | `ReactNode` | The content whose safe area changes. | | `spacing` | `number` | Optional gap between accessory and content. | Related: [Quick start](https://satin.build/docs/learn/quick-start.html), [List](https://satin.build/docs/components/list.html), [Screen](https://satin.build/docs/components/screen.html). --- Canonical page: https://satin.build/docs/components/zoomable.html Source: docs/components/zoomable.md # Zoomable and Selectable Content roles for a zoomable region or selectable text. ```tsx import { Selectable, Zoomable, Image } from '@satinhq/kit' ``` ## Example Select the meeting point text. Try pinching the image on a device. ```tsx import { Image, List, Screen, Section, Selectable, Zoomable } from '@satinhq/kit' function Example() { return (

Meeting point: Kyoto Station, north entrance.

Satin preview icon
) } ``` ## Allow text selection Wrap readable text in Selectable. Text inputs own their editing behavior separately. ## Own a zoom region Zoomable owns pinch zoom for its children. `maxScale` bounds the scale, and `onScaleChange` reports it. Try touch gestures in the installed app. ## API | Prop | Type | Description | | --- | --- | --- | | `children` | `ReactNode` | The owned content. | | `maxScale` | `number` | Maximum zoom scale. | | `onScaleChange` | `(scale: number) => void` | Receives scale changes. | Related: [Quick start](https://satin.build/docs/learn/quick-start.html), [List](https://satin.build/docs/components/list.html), [Screen](https://satin.build/docs/components/screen.html). --- Canonical page: https://satin.build/docs/glass/using-glass.html Source: docs/glass/using-glass.md # Using glass Glass, drawn by Lumen behind controls and other content. It bends and tints whatever is underneath as the page moves. ```tsx import { Glass, GlassEffect, GlassEffectContainer, Button, LabeledContent } from '@satinhq/kit' ``` ## Example Click Add Stop inside the glass effect. ```tsx import { useState } from 'react' import { Button, Glass, GlassEffect, GlassEffectContainer, LabeledContent, List, Screen, Section, } from '@satinhq/kit' function Example() { const [count, setCount] = useState(0) return (
) } ``` ## Choose the material Use Glass.regular or Glass.clear. The value supports `.interactive()` and `.tintRGBA([r, g, b, a])` with normalized channels between 0 and 1. ## Group effects Put related GlassEffect views in a GlassEffectContainer. Give effects a `glassEffectID` and `in={namespace}` from `useNamespace()` when they need shared transition identity. `id` remains the DOM id. `glassEffectID` requires a `NamespaceID` passed through `in`. Give related effects the same namespace and distinct identities. Retain each identity across renders; use the same identity at two alternate locations only when they represent the same effect. Omitting the namespace when supplying an identity is rejected. An effect without shared identity, like the example above, needs neither prop. ```tsx import { GlassEffect, GlassEffectContainer, Text, useNamespace } from '@satinhq/kit' export function JourneyControls() { const namespace = useNamespace() return ( Departure Arrival ) } ``` Glass, matched geometry and navigation transitions use the same `NamespaceID` values returned by `useNamespace` or `defineNamespace`. ## Provide readable pixels Cross-origin frames, images without CORS and DRM video cannot supply pixels to glass. Read [Known limitations](https://satin.build/docs/glass/limitations.html) before overlapping those sources. ## API | Prop | Type | Description | | --- | --- | --- | | `glass` | `Glass` | Regular by default. | | `shape` | `GlassShape` | Capsule by default. | | `glassEffectID` | `string` | Morph identity, separate from DOM id. | | `glassEffectTransition` | `matchedGeometry \| identity` | `glassEffectTransition` | An optional transition policy. | | `in` | `NamespaceID` | The shared identity namespace. | | `glassEffectUnion` | `string` | An optional union group. | | `spacing` | `number` | Grouping spacing on the container. | Related: [Quick start](https://satin.build/docs/learn/quick-start.html), [List](https://satin.build/docs/components/list.html), [Screen](https://satin.build/docs/components/screen.html). --- Canonical page: https://satin.build/docs/components/navigation-stack.html Source: docs/components/navigation-stack.md # NavigationStack A navigation container for moving between declared screens. ```tsx import { NavigationStack, NavigationLink } from '@satinhq/kit' ``` ## Example Click Kyoto, change Visited, then click the back button. ```tsx import { useState } from 'react' import { List, NavigationLink, Screen, Section, Toggle, defineApp, defineNavigation, defineRoutes, param, route, runApp, type RouteParameters, } from '@satinhq/kit' import { app as appInput } from './app' const destinationParams = { name: param.string() } function Places() { return (
Kyoto Osaka
) } function Place({ name }: RouteParameters) { const [visited, setVisited] = useState(false) return (
) } const routes = defineRoutes({ places: route(Places, { title: 'Places', titleDisplay: 'large' }), place: route(Place, { params: destinationParams }), }) const navigation = defineNavigation(routes, { stack: { root: 'places' } }) declare module '@satinhq/kit' { interface Register { routes: typeof routes navigation: typeof navigation } } runApp({ app: defineApp(appInput), routes, navigation, }) ``` ## Declare the routes Use `defineRoutes` and `route` to name each screen. Declare typed parameters with `param` and register the routes so NavigationLink checks destinations. ## Choose the root Declare the primary root in `defineNavigation`. Without custom `content`, `runApp` renders the declared stack. Render `NavigationStack` or `NavigationSplitView` yourself when supplying custom app content. ## Open a destination NavigationLink uses `to` and `params`. The framework owns the in-memory stack and the back button. Try the left-edge back swipe on a device. ## API | Prop | Type | Description | | --- | --- | --- | | `tab` | `TabId` | Selects a declared primary tab. | | `modal` | `true` | NavigationStack can own a modal stack. | | `root` | `RouteId` | Required only for a modal stack. | | `to` | `RouteId` | A declared destination on NavigationLink. | | `params` | `RouteParameters` | Typed destination parameters. | Related: [Quick start](https://satin.build/docs/learn/quick-start.html), [List](https://satin.build/docs/components/list.html), [Screen](https://satin.build/docs/components/screen.html). --- Canonical page: https://satin.build/docs/components/navigation-split-view.html Source: docs/components/navigation-split-view.md # NavigationSplitView A navigation container with a sidebar and detail at regular width. ```tsx import { NavigationSplitView } from '@satinhq/kit' ``` ## Example Click Kyoto, change Visited, then click the back button. ```tsx import { useState } from 'react' import { List, NavigationLink, NavigationSplitView, Screen, Section, Toggle, defineApp, defineNavigation, defineRoutes, param, route, runApp, type RouteParameters, } from '@satinhq/kit' import { app as appInput } from './app' const destinationParams = { name: param.string() } function Places() { return (
Kyoto Osaka
) } function Place({ name }: RouteParameters) { const [visited, setVisited] = useState(false) return (
) } const routes = defineRoutes({ places: route(Places, { title: 'Places', titleDisplay: 'large' }), place: route(Place, { params: destinationParams }), }) const navigation = defineNavigation(routes, { stack: { root: 'places' } }) declare module '@satinhq/kit' { interface Register { routes: typeof routes navigation: typeof navigation } } runApp({ app: defineApp(appInput), routes, navigation, content: () => , }) ``` ## Declare the routes Use `defineRoutes` and `route` to name each screen. Declare typed parameters with `param` and register the routes so NavigationLink checks destinations. ## Choose the root Declare the primary root in `defineNavigation`. Render NavigationStack or NavigationSplitView from runApp when supplying a custom app view. ## Open a destination NavigationLink uses `to` and `params`. The framework owns the in-memory stack and the back button. Try the left-edge back swipe on a device. ## API | Prop | Type | Description | | --- | --- | --- | | `tab` | `TabId` | Selects a declared primary tab. | | `modal` | `true` | NavigationStack can own a modal stack. | | `root` | `RouteId` | Required only for a modal stack. | | `to` | `RouteId` | A declared destination on NavigationLink. | | `params` | `RouteParameters` | Typed destination parameters. | Related: [Quick start](https://satin.build/docs/learn/quick-start.html), [List](https://satin.build/docs/components/list.html), [Screen](https://satin.build/docs/components/screen.html). --- Canonical page: https://satin.build/docs/components/tab-view.html Source: docs/components/tab-view.md # TabView A container for primary tabs, each with its own navigation stack. ```tsx import { TabView } from '@satinhq/kit' ``` ## Example Click Favorites, then Library. Each tab keeps its own screen. ```tsx import { useState } from 'react' import { List, Screen, Section, TabView, Toggle, defineApp, defineNavigation, defineRoutes, route, runApp, } from '@satinhq/kit' import { app as appInput } from './app' function Library() { const [downloaded, setDownloaded] = useState(true) return (
) } function Favorites() { return (
Persuasion
) } function App() { return } const routes = defineRoutes({ library: route(Library, { title: 'Library', titleDisplay: 'large' }), favorites: route(Favorites, { title: 'Favorites', titleDisplay: 'large', }), }) const navigation = defineNavigation(routes, { tabs: [ { id: 'library', label: 'Library', symbol: 'book', root: 'library' }, { id: 'favorites', label: 'Favorites', symbol: 'star', root: 'favorites', }, ], }) declare module '@satinhq/kit' { interface Register { routes: typeof routes navigation: typeof navigation } } runApp({ app: defineApp(appInput), routes, navigation, content: App }) ``` ## Declare the tabs Register tab ids and root routes in `defineNavigation`. Use its `defaultTab` option for the initial tab. TabView renders those declarations, each with its own retained stack. ## Control selection Omit `selection` to let the TabView manage it. Supply `selection` and `onSelectionChange` for a controlled tab. ## Adapt to larger windows `tabViewStyle="sidebarAdaptable"` enables the supported sidebar adaptation. The compact preview shows the phone arrangement. ## Add a bottom accessory Pass persistent content, such as a mini player, to `bottomAccessory`. TabView places it relative to the tab bar, reserves its space in tab content, and owns safe-area clearance and movement with the tab chrome. It also adjusts placement for the supported iPad sidebar layout. Keep the accessory in this slot; do not position it over each tab or add another bottom inset to the tab screens. On iPhone, the expanded accessory occupies a fixed 48-point row. Taller app content does not enlarge that row. On iPad, the slot has a 48-point minimum and can grow for taller content. Keep the phone composition compact. TabView supplies regular capsule glass by default. Your app supplies the content, internal spacing and actions. When the app draws its own material, set `bottomAccessoryMaterial="custom"` to avoid a second glass wrapper. Return `` from `App` in the tabs example above. The button toggles local state here; connect it to your media player's playback action in your app. ```tsx import { useState } from 'react' import { Button, HStack, TabView, Text, VStack } from '@satinhq/kit' export function PlayerTabs() { const [playing, setPlaying] = useState(false) return ( Evening walk Harbor sessions
) } ``` ## Choose the heights `detents` lists the heights the sheet can rest at, from shortest to tallest. The default is `['large']`, a full-height card. A detent is `'medium'`, `'large'`, `{ height }` in points, or `{ fraction }` of the available height. ```tsx
``` To know or set the current height, pass `selection` and `onSelectionChange`. ## Show or hide the grabber By default the grabber appears only when there is more than one detent to drag between, as in native iOS. Use `dragIndicator="visible"` or `"hidden"` to choose. ## Keep the sheet open while editing `interactiveDismissDisabled` stops the swipe-down gesture, so unsaved edits are not lost by accident. Give the person a Cancel or Done button instead. ```tsx
``` ## Close the sheet from inside Call `useDismiss()` in a component inside the sheet. It closes the sheet, even when swipe-down is disabled. In a screen pushed inside the sheet, it goes back one screen instead. ```tsx import { useState } from 'react' import { Button, List, Screen, Section, Sheet, useDismiss } from '@satinhq/kit' function DoneButton() { const dismiss = useDismiss() return } ``` ## Navigate inside a sheet Put `` inside the sheet. The sheet gets its own title bar, toolbar and back stack, separate from the screen behind it. See the [short sheets recipe](https://satin.build/docs/recipes/index.html#short-sheets-with-independent-navigation). ## Good to know - Sheets start closed when the app relaunches, as in native apps. Keep `isPresented` in `useState`, not in `useSceneStorage`. Save drafts separately. - The `title` is drawn as the sheet's header. `titleVisibility="hidden"` keeps it only as the accessible name, for content that fills the card. ## API | Prop | Type | Description | | --- | --- | --- | | `isPresented` | `boolean` | Whether the sheet is open. Required. | | `onPresentedChange` | `(presented: boolean) => void` | Called when the person dismisses the sheet. Required. | | `title` | `string` | Header text and accessible name. Required. | | `children` | `ReactNode` | What the sheet shows. | | `detents` | `PresentationDetent[]` | Resting heights. Default `['large']`. | | `selection` | `PresentationDetent` | The current detent, when you control it. | | `onSelectionChange` | `(detent) => void` | Called when the person drags to another detent. | | `dragIndicator` | `'automatic' \| 'visible' \| 'hidden'` | Grabber visibility. Default `'automatic'`. | | `interactiveDismissDisabled` | `boolean` | Turns off swipe-to-dismiss. Default `false`. | | `titleVisibility` | `'visible' \| 'hidden'` | Hides the drawn header. Default `'visible'`. | | `onDismiss` | `() => void` | Called after the sheet has closed. | `PresentationDetent` is `'medium' | 'large' | { height: number } | { fraction: number }`. Related: [FullScreenCover](https://satin.build/docs/components/full-screen-cover.html), [Alert](https://satin.build/docs/components/alert.html), `useDismiss`. --- Canonical page: https://satin.build/docs/glass/limitations.html Source: docs/glass/limitations.md # Glass: known limitations Satin draws glass by reading the pixels behind it. Browsers refuse to share some pixels with a page, so glass can't show them. This page lists those cases and what to do instead. ## Content glass can't show | Behind the glass | Why | What to do | | --- | --- | --- | | An `