Documentation

Layouts

UI that stays while the screen under it changes: a top bar, a tab bar, a shell. Layouts nest by folder, and a screen can opt out of them.

+layout.wh

// src/routes/+layout.wh
<Column class="w-full h-full safe">
  <TopBar />
  <slot />
</Column>

A +layout.wh wraps every screen in its folder and below. <slot /> is where the screen goes. Only a layout takes the page below it: <slot /> in a screen is a build error, since the router passes a screen no children. A +layout.load.wh beside it loads data for the whole chain; see $data.

See it happen

Layout.wh
<Column class="w-full h-full safe">
  <Text class="text-lg font-bold">My App</Text>
  <slot />
</Column>
Kotlin waiting
Scroll into view to compile.

The layout wraps whatever screen the router mounts at <slot />. class="safe" keeps the first row out from under the status bar.

Nesting

Layouts nest by directory, outermost first:

src/routes/
├── +layout.wh              # wraps everything
├── +screen.wh              # /
└── settings/
    ├── +layout.wh          # wraps /settings and below, inside the root layout
    ├── +screen.wh          # /settings
    └── profile/+screen.wh  # /settings/profile

/settings/profile renders inside the settings layout, which renders inside the root layout.

Opting out

An @ in the file name says which layouts to leave behind:

FileGets
+screen@.whno layouts
+screen@root.whonly the root layout
+layout@.whno layouts above it: a layout that starts a new chain

A login screen that should not carry the app's tab bar is +screen@.wh.

Route groups

src/routes/
├── (auth)/
│   ├── +layout.wh          # a layout for the group; no path segment
│   ├── login/+screen.wh    # /login
│   └── account/+screen.wh  # /account
└── about/+screen.wh        # /about

A folder in parentheses groups screens under a shared layout and adds nothing to the URL. (auth)/account/+screen.wh is /account. The group's layout is named after it, here AuthLayout. Groups are how two screens at the same depth get different layouts.

Safe area

Android draws under the status bar. A root layout wants class="safe"; without it the first row sits under the clock. bleed says the overlap is deliberate. The linter asks for one or the other on a root layout.

<Column class="w-full h-full safe">…</Column>    // content stays clear of the system bars
<Box class="w-full h-full bleed">…</Box>          // the overlap is intended

One edge, or the keyboard, at a time, with safe-top, safe-bottom and safe-ime. See Layout.

The shell: src/main.wh

Layouts wrap screens. src/main.wh wraps the router itself, one level up. It is optional; its body wraps every screen, and the router mounts at <slot />. <App>'s props choose the theme, and bound to a store they switch it while the app runs:

import $lib.stores.Settings

<App theme={Settings.mode}>
  <Column class="w-full h-full">
    <OfflineBanner />
    <slot />
  </Column>
</App>

Absent, the shell is <App><slot /></App> and [theme] mode in whitehall.toml is the scheme. A body without <slot /> is a build error.

Use a layout for what belongs to some screens and a shell for what belongs to the whole app, including the ones that skip every layout with +screen@.wh.

See Also