Default
Layout is the original full-screen app shell. Has built-in scroll state, loading screens, top nav, sticky footer. Defaults to a mobile viewport — use the device switcher in the preview to see how it adapts.
import {
Body1,
Body2,
Card,
Flex,
Layout,
PrimaryButton,
Title3,
Toast,
} from "@appboxo/ui-kit"
/**
* Layout is full-screen by design — it sets height: 100dvh on its root
* and assumes it owns the viewport. The docs site renders this preview
* inside an iframe whose container is sized to the chosen device
* (mobile / tablet / desktop), so Layout fills it naturally.
*/
export function LayoutDefaultPreview() {
return (
<Layout
navBar={{
title: "Checkout",
hideBack: false,
}}
footer={
<PrimaryButton
text="Confirm payment"
onClick={() => Toast.info("Confirmed")}
/>
}
>
<Flex vertical gap={16}>
<Card>
<Flex vertical gap={6}>
<Title3>Order summary</Title3>
<Body2 color="text-3">Lounge pass · KUL · Plaza Premium</Body2>
</Flex>
</Card>
<Card>
<Flex vertical={false} justify="space-between" align="center">
<Body1>Subtotal</Body1>
<Body1 weight="semibold">$117.60</Body1>
</Flex>
</Card>
<Card>
<Body2 color="text-3">
Your card on file will be charged once you confirm.
</Body2>
</Card>
</Flex>
</Layout>
)
}
With loading state
Pass screenState={{ isLoading: true }} + a loadingNode={<LayoutLoading />} for the initial render of a route.
import { Layout, LayoutLoading } from "@appboxo/ui-kit"
export function LayoutLoadingDefaultPreview() {
return (
<Layout
navBar={{ title: "Loading…" }}
screenState={{ isLoading: true }}
loadingNode={<LayoutLoading />}
>
<div />
</Layout>
)
}
When to use
Layout— original full-screen app shell. Owns viewport, has built-in scroll state.ResponsiveLayout— newer responsive shell that adds a desktop layer.
NavBar scrollTitle
The iOS-style "big title in the content area, small title slides into the nav on scroll"
pattern. Set navBar.scrollTitle to any ReactNode and it takes over once the user
scrolls past scrollTitleThreshold pixels (default 20).
<Layout
navBar={{
title: null,
scrollTitle: <Headline weight="bold">Checkout</Headline>,
scrollTitleThreshold: 20,
}}
header={<LargeTitle weight="bold">Checkout</LargeTitle>}
>
…
</Layout>The kit tracks scroll via Arco's getComputedStyleByScroll hook, so the default scroll
shadow keeps working. Apps that already pass their own getComputedStyleByScroll see it
wrapped, not replaced.
Default error placeholder
When screenState.isError flips to true and the consumer hasn't supplied an explicit
errorPlaceholder, the kit now falls back to a sensible generic message ("Something went
wrong / Please try again in a moment.") plus a "Try again" button wired to
screenState.refetch when present.
Previously the kit silently rendered nothing on error — a footgun for first-time
consumers. Apps that want a custom error graphic, brand-specific copy, or different copy
should still pass their own errorPlaceholder to override.
<Layout
screenState={{ isError, refetch }}
// No errorPlaceholder → kit ships a generic Try-again fallback.
>
…
</Layout>Installation
pnpm add @appboxo/ui-kitTypes
Layout, LayoutProps, NavBarProps, ScreenStateProps are all exported.
combineHooksResultsToLayoutScreenState helps merge multiple data-loading hooks into
Layout's screen state.