Components · Layout and structure

Page

The outer scaffold every product surface composes inside. Two variants — main and subpage — distinguished by their navigation contract: main pages live inside the app shell's top + side nav; subpages always carry a back or close button to return.

Documentedby Derek Fidler

Accounting

Connect your accounting software with Flatpay to automatically export your sales and transaction data.

Available in your region.

Pull settlement data into Xero, QuickBooks, or your accounting tool of choice. Once connected, every payout and refund flows through automatically.

Main page · Accounting

Overview

The Page lives in @flatpay-dk/ui as <Page> plus its slot children: <PageHeader> (or <PageHero> for hero-led main pages), <PageBody>, and <PageStickyFooter>. Every product route renders one Page at the top of its tree. The variant — main or subpage— is chosen by the route’s navigation context, not by visual preference.

One Page per route

A route renders one Page at the top of its tree. Pages don’t nest — if a section of a page needs its own header and back button, that section is asking to be a subpage of its own.

The split between main and subpage isn’t about typography — both variants render the H1 in Founders Grotesk uppercase, the brand display face. The split is about how the user got here and how they get out.

  • Main pages live inside the app shell. The top bar and side rail are always present, and they handle hierarchy — clicking a side-nav item is how the user moves between sections. Main pages never carry a back button; the app shell already covers that need.
  • Subpages always carry a back or close button. They sit one or more levels deeper than a main page — the user came from somewhere, and the page must give them a way back. The back button replaces the side rail visually while still letting them return.

Main page

The landing surface for a section: Dashboard, Sales, Accounting, Reports, Team. Renders inside the app shell, which provides the top bar and side nav. The Page component itself never draws those chrome elements — it composes inside them.

Accounting

Connect your accounting software with Flatpay to automatically export your sales and transaction data.

Available in your region.

Pull settlement data into Xero, QuickBooks, or your accounting tool of choice. Once connected, every payout and refund flows through automatically.

The app shell is a layout, not a page

In a Next.js app, the top + side nav usually live in a layout.tsx that wraps every page.tsx in the route group. The Page component is what you put inside that layout’s {children} slot. If your route is not inside the shell, it’s probably a subpage.

Main with hero

When the page leads with editorial content — a product landing, an onboarding step, a feature introduction — use <PageHero> in place of <PageHeader> and put a configured <Hero> inside it. The Hero block replaces the title row entirely; the slot provides the same responsive top + horizontal padding as the header so the Hero aligns with the body content column. Don't render both <PageHeader> and <PageHero> in the same page.

Capital

You’re prequalified for a cash advance starting from 500.000 kr

Whether you want to grow, evolve, or just maintain — get access to the funding that you deserve. Money in your account in 5–8 business days.

Fast approval, no paperwork

Skip the endless forms and waiting times.

Simple pricing, no hidden rates

One flat fee, no surprises or hidden costs.

Quick funding

Cash in your account within 5–8 days.

Subpage

Detail pages, edit forms, single-task surfaces. Always carry a backprop on the PageHeader — the component dev-warns if you forget. The title centers on the page (a 1fr · auto · 1fr grid balances the back affordance against any trailing actions), and is itself optional — a focused-task subpage can carry its meaning through the back button and body content alone. Subpages don’t render a description. Body content — banners, forms, lists — fills the column set by the Page’s widthprop; don’t add per-element max-widths inside, change width instead.

  • kind="back" — an arrow_back icon. Use when the subpage was reached from a list, a row click, or a navigation event that has a clear previous page (Sales → Sale detail, Team → Member detail).
  • kind="close" — an × icon. Use when the subpage feels like a focused task the user opened from anywhere (Add team member, Edit business hours). The close action implies “dismiss this task,” not “go up one level.”

Edit business hours

Available — changes save instantly to your storefront.

Monday09:00 — 17:00
Tuesday09:00 — 17:00
Wednesday09:00 — 17:00
Thursday09:00 — 17:00
Friday09:00 — 17:00
Saturday09:00 — 17:00
Sunday09:00 — 17:00

Add team member

Width

The header and the body share the same horizontal column, so the page title always aligns with the content beneath it. Three variants — pick by what the page is for, not by what fits.

  • width="full" — no max-width. Reach for it when the page is dense by nature: dashboards, transaction tables, anything that earns every pixel.
  • width="default" max-w-4xl (896 px). The standard page. Settings, accounting, most feature surfaces.
  • width="narrow" max-w-2xl (672 px). Long-form content and single-column forms — the eye tracks better at ~65 ch.

Accounting

Connect your accounting software with Flatpay so every payout and refund exports automatically.

full · no max-width — dashboards, tables, anything that wants the whole canvas

Accounting

Connect your accounting software with Flatpay so every payout and refund exports automatically.

default · max-w-4xl (896 px) — the standard page

Accounting

Connect your accounting software with Flatpay so every payout and refund exports automatically.

narrow · max-w-2xl (672 px) — long-form content and forms

No divider, no surprise

The header doesn’t carry a hairline separator. Title, description, and body read as one continuous column — consistent column width is what does the framing work, not a line.

Header options

Title is the only required prop on every header. description, status, and actions are each opt-in. back is required on subpages and ignored on main.

Accounting

Main · title only

Accounting

Connect your accounting software with Flatpay.

Main · title + description

Accounting

Connect your accounting software with Flatpay.

Main · full — title + description + actions

Edit business hours

Subpage · back + title (centered)

Edit business hours

Subpage · back + title + actions
Subpage · back only (no title)

Mount <PageStickyFooter> at the end of a subpage when there’s a save / cancel pair the user shouldn’t lose track of as they scroll. Two slots: leading for a destructive or escape action (Discard, Cancel), trailing for the commit (Save, Send invite). The footer pins to the bottom of the viewport with a 1 px top border and a translucent backdrop, and the parent Page reserves enough bottom padding so the footer never hides content.

Subpages only

Sticky footers belong on subpages — the surfaces where the user has a discrete commit to make. Main pages don’t commit changes inline; if a section action needs a save footer, it’s asking to be a subpage of its own.

Anatomy

A subpage at full chrome. The back button on the left, the title block in the middle, status + actions on the right; the body fills the remaining space; the sticky footer pins to the bottom.

Edit business hours

Available — changes save instantly to your storefront.

Monday09:00 — 17:00
Tuesday09:00 — 17:00
Wednesday09:00 — 17:00
Thursday09:00 — 17:00
Friday09:00 — 17:00
Saturday09:00 — 17:00
Sunday09:00 — 17:00

Accessibility

  • Single H1. The Page renders the headline as an <h1>. Don’t add another inside the body — every page has exactly one.
  • Main landmark. <PageBody> renders as <main> by default and wires its aria-labelledby to the page’s H1, so screen readers announce the page by its title.
  • Back button announces its meaning. The icon-only back button defaults its aria-label to “Back” or “Close” based on kind. Override with ariaLabelwhen the destination has a specific name (“Back to settings”).

Code

tsx

import {
  Page, PageHeader, PageBody, PageStickyFooter,
  Banner, Button, ButtonGroup,
} from "@flatpay-dk/ui";

// Main page — Founders hero, header actions, Banner for state
<Page variant="main" width="default">
  <PageHeader
    title="Accounting"
    description="Connect your accounting software with Flatpay."
    actions={
      <ButtonGroup>
        <Button variant="secondary">Cancel</Button>
        <Button>Connect</Button>
      </ButtonGroup>
    }
  />
  <PageBody>
    <Banner tone="success">Available in your region.</Banner>
    {/* content */}
  </PageBody>
</Page>

// Subpage — centered title, narrow width for a focused form.
// Subpages don't render a description, so don't pass one.
<Page variant="subpage" width="narrow">
  <PageHeader
    title="Edit business hours"
    back={{ href: "/settings", kind: "back" }}
  />
  <PageBody>{/* form */}</PageBody>
  <PageStickyFooter
    leading={<Button variant="tertiary">Discard</Button>}
    trailing={
      <ButtonGroup>
        <Button variant="secondary">Reset to default</Button>
        <Button>Save changes</Button>
      </ButtonGroup>
    }
  />
</Page>

// Subpage — title is optional. A close-only header anchored
// by the body works for focused-task surfaces.
<Page variant="subpage">
  <PageHeader back={{ onClick: () => router.back(), kind: "close" }} />
  <PageBody>{/* invite form */}</PageBody>
  <PageStickyFooter
    trailing={
      <ButtonGroup>
        <Button variant="tertiary">Cancel</Button>
        <Button>Send invite</Button>
      </ButtonGroup>
    }
  />
</Page>

Best practices

  • Pick the variant by the navigation context. In the side nav? Main. Reached by clicking a row, a link, or an “Add new” button on a previous page? Subpage.
  • Always wire the back button to a real destination. Prefer href over onClick when there’s a stable parent route — it gives the user middle-click and keyboard navigation for free. router.back() is fine for modal-feeling subpages where the parent depends on history.
  • Sticky footer means dirty form.A sticky footer that says “Save changes” while there are no changes is noise. Mount the footer when the form goes dirty; unmount when it goes back to clean.
  • Don’t double up the H1. The page already renders an H1. Section components inside the body should start at H2.

Props

<Page>

PropTypeDefaultDescription
variant"main" | "subpage""main"main lives inside the app shell's top + side nav. subpage replaces side-nav hierarchy with a back / close button.
width"full" | "default" | "narrow""default"Constrains header and body to the same centered column. full: no max-width. default: max-w-4xl (896 px). narrow: max-w-2xl (672 px).
children*ReactNodePageHeader + PageBody, optionally PageStickyFooter.

<PageHeader>

PropTypeDefaultDescription
titleReactNodeThe page H1, set in Founders Grotesk uppercase. Required on main pages, optional on subpages (focused-task surfaces can skip it). Centered on subpages, left-aligned on main.
descriptionReactNodeOptional supporting line. Renders only on main pages — subpages stay focused on the task and ignore the prop.
statusReactNodeOptional inline node next to the title — a Badge, a small label. For longer state messages (e.g. 'Available in your region') reach for a Banner inside the body instead.
actionsReactNodeOptional trailing actions — typically a ButtonGroup.
backPageBackButtonRequired on variant="subpage". Object with href and/or onClick, plus optional kind ("back" | "close"), label, and ariaLabel. Ignored on main.

<PageHero>

PropTypeDefaultDescription
children*ReactNodeThe top-of-page block — typically a configured <Hero>. The slot only provides positioning (responsive top + horizontal padding to align with the body content column).
classNamestringOptional class merged onto the slot wrapper. Use sparingly; the responsive padding is the slot's job.

<PageBody>

PropTypeDefaultDescription
as"main" | "section" | "div""main"Semantic element. Default <main> wires aria-labelledby to the page H1. Use 'section' or 'div' for any subsequent body in the same page.
children*ReactNodeThe body content.

<PageStickyFooter>

PropTypeDefaultDescription
leadingReactNodeLeft slot — typically a tertiary action like Discard or Cancel. Optional.
trailing*ReactNodeRight slot — the commit action, typically a Button or ButtonGroup.