[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/pythonnative/pythonnative/main/docs/guides/browser-preview.md [Back]  [Original]

# Browser preview

`pn preview` renders your app in a browser tab, inside a phone frame,
with **Fast Refresh on every save**. It's the fastest way to build UI in
PythonNative: edit a component, save, and see the result in well under a
second, with no simulator boot, no device deploy, and no native build.

```bash
pn preview
```

The browser renderer speaks the same bridge protocol as the Swift and
Kotlin renderers. Your components, hooks, reconciliation, logical navigation,
and async work run in the `pn preview` Python process. The page implements
widgets with DOM elements, layout with Yoga WebAssembly, and animation
graphs with JavaScript. Platform controls, fonts, and device APIs differ
from those in a mobile app.

## Quick start

From a project directory (one created by `pn init`, with an
`app/main.py` that defines `App`):

```bash
pn preview
```

This starts the [dev server](dev-workflow.md), opens
`http://localhost:8765/?token=...` in your default browser (the page
trades your [dev token](dev-workflow.md#the-dev-token) for a cookie and
drops it from the address bar), mounts your `App` in
a phone frame, and watches `app/` for changes. Edit any component and
save: the page updates in place while preserving component state
(counters, text input, scroll position, the navigation stack).

`pn preview` imports your real app code, so install your project's
dependencies in the same environment first (`pip install` whatever you
declared in `[requirements].packages`). `pn preview` warns about
missing ones on startup. If an import fails, the preview logs the
traceback in its console and the terminal; install the package or fix
the code and save to recover.

```bash
pn preview                        # the project entry point (app/main.py -> App)
pn preview app.screens.home       # mount a different module's App
pn preview --port 9000            # another port
pn preview --host 127.0.0.1       # don't expose the server on the LAN
pn preview --no-open              # start the server, open the tab yourself
pn start                          # the same server without opening a browser
```

`pn start` and `pn preview` are the same command; `pn preview` only
adds opening the tab. Simulators and devices launched with `pn run`
connect to the same server, so one terminal drives every client.

## The toolbar

Along the top of the page:

- **Device**: iPhone 15, iPhone 15 Pro Max, iPhone SE, Pixel 8, Pixel
  Fold (inner), iPad mini, or *Fill window*. The frame's size, safe
  area insets, and notch follow the choice, so `use_safe_area_insets`
  and `use_window_dimensions` return realistic values. Switching
  devices re-lays out the running app without remounting it.
- **Rotate** (`r`): swap width and height.
- **Dark / Light** (`d`): toggle the color scheme. `use_color_scheme`,
  `use_theme`, and `{"light": ..., "dark": ...}` dynamic colors all
  follow it live.
- **Back** (`Esc`): the system back action. `use_back_handler`
  subscribers on the active screen get the first chance to consume it;
  otherwise the navigation stack pops, matching the Android hardware
  back button.
- **Reload app** (`Shift+R`): tear the app down and mount it again from
  scratch, discarding all state (a full remount, not a Fast Refresh).
- **Console** (`` ` ``): show or hide a log panel with the same lines
  the terminal prints, plus the page's own errors.

The status dot shows the WebSocket connection. If you stop `pn preview`
the page waits and reconnects when the server returns; the app is
mounted fresh on reconnect.

## How it works

`pn preview` sets `PN_PLATFORM=web` and runs
[`pythonnative.preview.serve`][pythonnative.preview.serve], which:

1. Starts a [`DevServer`][pythonnative.devserver.DevServer] on a
   background thread: HTTP for the page and its assets, WebSocket for
   the page and for dev clients.
2. Installs a [`WebTransport`][pythonnative.bridge.web.WebTransport] as
   the bridge transport. Transactions the reconciler commits become
   `["apply", request_id, commit]` messages to the page; the page's events come back
   as `["cb", ...]` callbacks. Commits acknowledge exact revisions, and native
   Yoga WebAssembly layout returns a batch of changed frames.
3. Runs Python callbacks and tasks on the standard application asyncio loop.
   The page owns DOM widgets and Yoga WebAssembly layout. Network I/O and Python
   work execute independently of the browser's UI thread.
4. When the page connects, tells it the entry module. The page asks
   the `Host` module to create a screen, exactly as
   `PNViewController` and `PNScreenFragment` do, and a
   [`NativeScreenHost`][pythonnative.hosts.native.NativeScreenHost]
   mounts your `App`.
5. Watches `app/` through the dev server and applies each change with
   [`apply_reload`][pythonnative.hot_reload.apply_reload], the same
   function the on-device dev client uses.

Layout is owned by the engine, not the DOM: the
[flex layout engine](../concepts/layout.md) computes an absolute frame
for every element, and the page positions each element with that frame.
Text is measured by the page with the same font it renders, so wrapping
and intrinsic sizes match what you see.

### Navigation

Root navigators (`StackNavigator`, `TabNavigator`, and
`DrawerNavigator`) drive a real stack of screens in the page, the same
way they drive `UINavigationController` and fragments on device. Each pushed
screen remains a logical child of the application's shared tree;
`navigate(...)` selects or adds a route, `go_back()` pops, and covered
screens keep their state.
Pushes and pops animate with the screen's `animation` and
`presentation` options.

Each stack draws a navigation bar from the top screen's options, the way
the native stacks do: `title`, `header_shown`, `header_large_title`,
`header_back_visible`, `header_back_title`, `header_tint_color`,
`header_style`, and `header_title_style`, with `header_left` and
`header_right` rendered in the bar. The back button pops through the same
path as an iOS swipe back, so a `before_remove` listener can veto it. A
stack at the root of the app replaces the host's own bar, as it does on
iOS.

### Dev mode and errors

The preview always runs with dev diagnostics on: unknown style keys and
duplicate list keys print `[PN] WARN` messages, hook-order violations
raise immediately, and uncaught errors from renders, effects, and event
handlers show a full-screen **RedBox** with the traceback. Fix the code
and save; a successful reload clears the overlay. Errors that happen
before any screen exists (an import error in `app/main.py`) print in
the terminal and the page console.

## Fast Refresh

Saving a `.py` file under `app/` reloads the changed modules and swaps
every affected component function into the live tree in place, so the
next render reuses the existing hook state. Edits to a component body
keep your counters, form values, scroll positions, and navigation
stack. When a clean swap isn't possible, the screen remounts instead
so you're never stuck with a stale tree. See the
[Fast Refresh guide](hot-reload.md) for the mechanics; the preview and
device builds share the same engine.

## Branching on the platform

When the preview is running, [`Platform.OS`][pythonnative.Platform] is
`"web"`:

```python
import pythonnative as pn

pad = pn.Platform.select({"web": 12, "ios": 16, "android": 16, "default": 12})
```

`Platform.select`'s `"native"` key matches iOS and Android only; the
browser is a development surface, so use an explicit `"web"` key (or
`"default"`) for it. You can also check
[`Platform.is_web`][pythonnative.Platform] or the
`pythonnative.utils.IS_WEB` flag directly.

## What's faithful, and what's approximated

The preview is a **development tool**, optimized for fidelity of layout
and logic rather than pixel-perfect platform chrome.

Faithful:

- Flex layout, sizing, padding, gap, absolute positioning, and
  safe areas for the chosen device frame.
- Component lifecycle, hooks, effects, context, error boundaries,
  Suspense.
- Navigation (stack push and pop, tabs, drawer, modals) and per-screen
  state.
- The async runtime, `use_resource`, `use_query`, timers, and
  state-driven updates.
- Text wrapping and intrinsic sizing.
- Colors (hex, rgb(a), named, `{"light", "dark"}`), border radius,
  borders, opacity, shadows, and overflow clipping.
- `Animated` timing, spring, and decay, run through the same
  `animate` protocol the native animators implement; transforms,
  opacity, and colors update through the page's animation graph.
- Gestures: the page streams raw pointer events to the Python
  [gesture arbiter](gestures.md). Mobile renderers recognize gestures
  in Swift and Kotlin.
- Scrolling, `FlatList` / `SectionList` windowing, pull to refresh,
  and scroll-driven `Animated.event` bindings.

Approximated or absent:

- Fonts are the browser's system stack, not San Francisco or Roboto,
  so glyph metrics differ slightly.
- Native controls (`Switch`, `Slider`, `Picker`, `DatePicker`,
  `SegmentedControl`) are styled HTML inputs rather than platform
  widgets.
- `WebView` is an ``, which many sites refuse to load in.
- `Image` loads whatever the browser can (PNG, JPEG, GIF, WebP, SVG)
  from files under `app/` or URLs; asset resolution otherwise matches
  device builds.
- Device APIs: `Alert`, `Clipboard`, `Linking`, `Share`, `Haptics`
  (`navigator.vibrate`), `NetInfo`, `AppState`, and `Device` are
  implemented in the page. Everything else (`Camera`, `Location`,
  `Notifications`, `Biometrics`, `Storage`, ...) uses the pure-Python
  fallbacks in
  [`pythonnative.native_modules.fallback`](../api/native_modules.md):
  in-memory stores, `"unknown"` states, and "unavailable" results.
- Components without a browser implementation render a labeled
  placeholder box so the layout around them stays truthful.

When the chrome matters, verify on device with `pn run`.

## Sharing the preview

The server binds to all interfaces by default so your own phones can
reach it, but it only answers clients that present your
[dev token](dev-workflow.md#the-dev-token). The LAN URLs `pn start`
prints carry the token, and anyone who has one can read your app's
source for as long as the token stays the same, so open them only on
your own devices. One page drives the app at a time: the newest tab to
connect takes over, and the previous one shows a notice until you reload
it. Use `--host 127.0.0.1` to keep the preview local.

## Next steps

- The whole loop, including simulators and devices:
  [Development workflow](dev-workflow.md).
- Mechanics shared with device Fast Refresh: [Fast Refresh](hot-reload.md).
- How layout is computed: [Layout engine](../concepts/layout.md).
- Platform branching: [Platform & accessibility](platform-accessibility.md).

Web Proxy Viewer  |  New URL  |  Original Page