place

layout()

A layout is a page-shaped value that takes children. Compose them outside-in to share chrome across many pages.

Basic

ts
import { layout } from '@place/component' export const rootLayout = layout({ meta: { bodyClass: 'bg-bg text-fg' }, view: ({ children }) => ( <div class="min-h-screen flex flex-col"> <Header /> <main class="flex-1">{children}</main> <Footer /> </div> ), })

load — server-side data

ts
export const dashLayout = layout({ load: async ({ req }) => ({ user: await getUserFromRequest(req), }), view: ({ children, user }) => ( <div> <UserBadge name={user.name} /> {children} </div> ), })

Layout load runs server-side before view(). Its return merges into loadData which flows to every layout and the page in the chain.

Layout chains

ts
// Layouts compose outside-in: page('/admin/users', { layout: [rootLayout, adminLayout], view: ({ children }) => <UserList />, }) // rootLayout wraps adminLayout wraps the page. // Both layouts' load() run in chain order; results merge into a single // loadData passed to every view + meta callback in the chain.

Typed named slots

Layouts can expose typed named slots — regions a page can fill with custom content. The layout's second type parameter declares the slot key union; pages get TypeScript autocomplete on those keys. No file convention, no @-prefixed parallel routes, no <NuxtPage /> magic.

ts
// Typed named slots — let pages customize specific layout regions // without parallel-route file conventions (Next.js) or magic single // outlets (Nuxt). The layout's second type parameter declares the // slot key union; pages get autocomplete on those keys. const dashboard = layout<{}, 'headerActions' | 'sidebar'>({ view: ({ children, slots }) => ( <div class="grid grid-cols-[240px_1fr]"> <aside class="border-r"> {slots('sidebar') ?? <DefaultSidebar />} </aside> <main> <header class="flex justify-end gap-2 p-2"> {slots('headerActions')} </header> {children} </main> </div> ), }) // Each page fills the slots it cares about. Missing slots resolve // to null; the layout can branch on `slots.has('name')` for fallbacks. page('/users', { layout: dashboard, slots: { headerActions: () => <NewUserButton />, sidebar: () => <UserFilters />, }, view: () => <UserList />, }) page('/settings', { layout: dashboard, // No `headerActions` slot — that region renders null. slots: { sidebar: () => <SettingsNav /> }, view: () => <SettingsForm />, })

Slots are functions (lazy), so the layout decides when to render them — branching with slots.has('name') covers the "render a fallback when unfilled" case without cost. Slot fills survive the whole layout chain: an outer layout doesn't need to know what its inner layouts consume.

Compared to other frameworks: Next.js requires parallel routes (file-system magic, @modal/page.tsx directories) for the same UX. Nuxt has one <NuxtPage /> outlet per layout — multi-slot isn't first-class. Remix's <Outlet /> is single-slot too. place's named slots are typed values flowing through props.

See also