A viewport-filling centered page flow for sign-in, sign-up, onboarding, 404, and other focused full-page states. It is a layout: it owns where things go (the flex/overflow skeleton and region ordering), not what they are. App state (routing, sessions, data) never enters it; the brand mark, form card, and footer links arrive as composed JSX. It consolidates the centered "auth shell" that was duplicated across ngrok's dashboard and login apps. The demo runs in its own framed document, exactly like a real sign-in page: the layout owns the document, so it composes a real Main landmark and a skip link (see Landmarks).
Composition
Rootis the outer frame. It fills its nearest sized ancestor (min-h-full) and stacksNoticeandHeader(pinned top),Body(which grows), andFooter(pinned bottom). Mergeh-fullviaclassNamewhen the host requires an exact height —cxis tailwind-merge-backed, so the override is deterministic.Noticeis a full-window-width strip pinned above everything else — includingHeader— for maintenance notices, environment warnings, and similar app-wide messaging. It is unstyled (w-full shrink-0): the notice content brings its own colors, and the part collapses to nothing when empty. The same slot contract asAppLayout.Notice, so an app-wide banner composes identically across mantle's layouts; see Notice. Optional.Headeris a utility strip at the top (an account chip, a close button) rendered outside the centered region as a semantic<header>, the mirror ofFooter. Optional; see Header for the window-pinned variant.Bodyis the growing, centered region (flex-1, centered,gap-6). By convention the brand mark (a fully-styled logo/wordmark anchor) is its first child, and the primary content follows as composed JSX (adiv, or the Main landmark when the layout owns the document; see Landmarks). There is deliberately noLogoorContentpart because they would own nothing: vertical spacing comes fromBody'sgap-6, and both arrive as already-styled JSX. Content taller than the viewport top-flows and the page scrolls, becauseRootgrows instead of clipping.Footeris a pinned utility strip (theme switcher, legal links) rendered outside the centeredflex-1region — which is exactly whyBodyexists as a separate part:Rootcannot own the centering becauseHeaderandFootermust sit outside it.Footeris optional; omitting it is fine.
Header
Focused flows that the user can leave (checkout, plan pickers, onboarding steps) carry a utility strip at the top: who is signed in on the left, a close button on the right. Header is that strip, the mirror of Footer. To keep the close button reachable while the page scrolls, merge sticky top-0 z-10 (and a background) via className — prefer sticky over fixed, which removes the strip from flow and underlaps the content below it. The demo runs in its own framed document, so its page really does own the scroller; scroll it to see the header stay pinned. On a real page, give the document scroller overscroll-none to suppress rubber-banding and pull-to-refresh; don't set it on a nested scroller, where cutting scroll chaining swallows the wheel at the scroller's edge instead of handing it back to the page.
Notice
App-wide messaging (a maintenance notice, an environment warning) belongs in a strip pinned above
everything else on the page, including the header. Notice is that strip: the same slot contract as
AppLayout.Notice, so a banner composes identically across
mantle's layouts. It is unstyled (w-full shrink-0): the notice content brings its own colors, and it collapses to
nothing when empty, so it can stay mounted and conditionally render its contents. Toggle the notice in the
demo to see it push the whole page (header included) down. On flows that scroll, it scrolls away with the
page by default; merge sticky top-0 z-20 via className to pin it to the window. To keep a sticky
Header pinned as well, wrap the two strips in a single sticky top-0 container — two
elements pinned at top-0 overlap each other once the page scrolls.
Landmarks
Exactly one element in a document may be the Main landmark, so the layout does not render one for you: content arrives as composed JSX, and composing the layout inside a shell that already owns the landmark (an app frame, or this page's inline Polymorphism example) is safe by default. When the layout owns the document (a sign-in page, a 404), compose mantle's Main directly in Body, and pair a SkipToMainLink as the first child of Root: its default targetId matches Main's id="main", so keyboard users can jump past the brand mark straight to the content. The framed demos on this page do exactly this: each preview is its own document, so each owns a real Main.
Polymorphism
Every part supports asChild to compose its structure onto alternative element types: here Root renders as a labelled <section>.
Check your email for a reset link.
Accessibility
- One Main landmark per document. Two
<main>elements in one document is invalid HTML and confuses screen-reader landmark navigation, so the layout renders none by default. Compose Main directly inBodywhen the layout owns the document (see Landmarks); inside a shell that already renders one, use a plaindivand no duplicate can occur. - Skip link pairing. The composed
Mainis focusable (id="main",tabIndex={-1}), so a SkipToMainLink placed as the first child ofRootsends keyboard users directly to the content. - Header and Footer landmarks.
Headeris a semantic<header>(banner) andFootera semantic<footer>(contentinfo) for the page-level frame, unlikeCard.Footer's genericdiv. These mappings only apply when the element is not nested inside a sectioning element, so renderingRootas a<section>viaasChild(as in the Polymorphism example) scopes both to that section instead.
API Reference
CenteredLayout.Root
The outer frame of the centered page flow. Fills its nearest sized ancestor and stacks Notice, Header, Body, and Footer.
All props from div, plus:
CenteredLayout.Notice
A full-window-width strip pinned above everything else in the layout — including CenteredLayout.Header — for maintenance notices, environment warnings, and similar app-wide messaging. Renders an unstyled <div> (w-full shrink-0): the notice content brings its own colors and layout, and the part collapses to nothing when empty, so it can stay mounted and conditionally render its contents. Deliberately not named Banner — CenteredLayout.Header renders the page's <header> (the ARIA banner landmark), and this part is not that. On flows that scroll, it scrolls away with the page by default; merge sticky top-0 z-20 via className to pin it to the window. To keep a sticky
Header pinned as well, wrap the two strips in a single sticky top-0 container — two
elements pinned at top-0 overlap each other once the page scrolls. Optional.
All props from div, plus:
CenteredLayout.Header
A utility strip rendered as a semantic <header> at the top of the layout, outside the centered region. Merge sticky top-0 z-10 via className to pin it to the window while the page scrolls. Optional.
All props from header, plus:
CenteredLayout.Body
The growing, centered region of the layout. By convention the brand mark is its first child.
All props from div, plus:
CenteredLayout.Footer
A pinned utility strip rendered as a semantic <footer> outside the centered region. Optional.
All props from footer, plus: