Billing and plan
Current plan, payment method and invoices.
Built with
- Badge
- Button
- IconButton
- PaymentCard
- RadioCard
- RadioGroup
- SavedCard
- Table
- TableBody
- TableCell
- TableHeader
- TableHeaderCell
- TableRow
Billing and plan
48 people on Growth: £384.00 a month. Next invoice on 1 November 2026.
Plan
Changes apply from your next invoice, pro-rated by day.Payment method
Northwind corporate
card ending 4417- Card holder
- SARA MALIK
- Expires
- 09/29
Invoices
| Invoice | Date | Status | Amount | Download |
|---|---|---|---|---|
NW-2026-010 | 1 October 2026 | Due 15 Oct | £384.00 | |
NW-2026-009 | 1 September 2026 | Paid | £368.00 | |
NW-2026-008 | 1 August 2026 | Paid | £368.00 | |
NW-2026-007 | 1 July 2026 | Paid | £352.00 |
billing-and-plan.tsx
"use client";
import { useState } from "react";
import { LuBuilding, LuDownload, LuRocket, LuSprout } from "react-icons/lu";
import {
Badge,
Button,
IconButton,
PaymentCard,
RadioCard,
RadioGroup,
SavedCard,
Table,
TableBody,
TableCell,
TableHeader,
TableHeaderCell,
TableRow,
} from "@nexera-ui/react";
const money = new Intl.NumberFormat("en-GB", { style: "currency", currency: "GBP" });
const PLANS = [
{
id: "starter",
name: "Starter",
icon: <LuSprout />,
price: 4,
text: "Directory, leave and documents for up to 25 people.",
},
{
id: "growth",
name: "Growth",
icon: <LuRocket />,
price: 8,
text: "Adds payroll in 4 countries, reviews and calendar sync.",
},
{
id: "scale",
name: "Scale",
icon: <LuBuilding />,
price: 14,
text: "Adds multiple entities, SSO, audit log and a success manager.",
},
];
const INVOICES = [
{ id: "NW-2026-010", date: "1 October 2026", amount: 384, status: "Due 15 Oct" },
{ id: "NW-2026-009", date: "1 September 2026", amount: 368, status: "Paid" },
{ id: "NW-2026-008", date: "1 August 2026", amount: 368, status: "Paid" },
{ id: "NW-2026-007", date: "1 July 2026", amount: 352, status: "Paid" },
];
/** Billing and plan: change plan, choose the payment method and download invoices. */
export default function BillingAndPlan() {
const [plan, setPlan] = useState("growth");
const seats = 48;
const chosen = PLANS.find((item) => item.id === plan) ?? PLANS[1];
if (!chosen) return null;
return (
<div className="@container min-h-[640px] bg-page px-4 py-8 @3xl:px-8">
<div className="mx-auto grid max-w-4xl gap-6">
<header className="grid gap-1">
<h2 className="text-heading-h2 text-primary">Billing and plan</h2>
<p className="text-body-default text-secondary">
{seats} people on {chosen.name}: {money.format(chosen.price * seats)} a month. Next
invoice on 1 November 2026.
</p>
</header>
<section className="grid gap-4 rounded-xl border border-default bg-surface p-5">
<RadioGroup
type="card"
label="Plan"
description="Changes apply from your next invoice, pro-rated by day."
name="plan"
value={plan}
onChange={setPlan}
>
{PLANS.map((item) => (
<RadioCard
key={item.id}
value={item.id}
icon={item.icon}
title={`${item.name} · ${money.format(item.price)} per person a month`}
meta={
item.id === "growth" ? (
<Badge label="Current plan" tone="brand" variant="solid" dot={false} />
) : undefined
}
description={item.text}
/>
))}
</RadioGroup>
{plan !== "growth" ? (
<Button size="md" className="justify-self-end">
Switch to {chosen.name}
</Button>
) : null}
</section>
<section
aria-labelledby="payment-heading"
className="grid gap-4 rounded-xl border border-default bg-surface p-5"
>
<h3 id="payment-heading" className="text-heading-h4 text-primary">
Payment method
</h3>
<div className="grid gap-5 @3xl:grid-cols-[auto_minmax(0,1fr)] @3xl:items-start">
<PaymentCard
variant="midnight"
cardName="Northwind corporate"
holder="SARA MALIK"
number="4000 0000 0000 4417"
expiry="09/29"
/>
<RadioGroup
type="card"
aria-labelledby="payment-heading"
name="payment-method"
defaultValue="corporate"
>
<SavedCard
value="corporate"
title="Corporate card ending 4417"
subtitle="Expires 09/29 · Sara Malik"
defaultBadge="Default"
/>
<SavedCard
value="finance"
title="Finance card ending 8804"
subtitle="Expires 02/28 · Chloe Martin"
/>
<SavedCard
value="old"
title="Card ending 1209"
subtitle="Expired 03/26 · Sara Malik"
status="expired"
/>
</RadioGroup>
</div>
</section>
<section
aria-labelledby="invoices-heading"
className="grid gap-4 rounded-xl border border-default bg-surface p-5"
>
<h3 id="invoices-heading" className="text-heading-h4 text-primary">
Invoices
</h3>
<Table aria-labelledby="invoices-heading">
<TableHeader>
<TableHeaderCell id="invoice" isRowHeader label="Invoice" />
<TableHeaderCell id="date" label="Date" />
<TableHeaderCell id="status" label="Status" />
<TableHeaderCell id="amount" align="right" label="Amount" />
<TableHeaderCell id="download" align="right" label="Download" />
</TableHeader>
<TableBody>
{INVOICES.map((invoice) => (
<TableRow key={invoice.id} id={invoice.id}>
<TableCell>{invoice.id}</TableCell>
<TableCell>{invoice.date}</TableCell>
<TableCell type="badge">
<Badge
label={invoice.status}
tone={invoice.status === "Paid" ? "success" : "warning"}
/>
</TableCell>
<TableCell type="number" value={money.format(invoice.amount)} />
<TableCell type="actions">
<IconButton
variant="ghost"
icon={<LuDownload />}
aria-label={`Download ${invoice.id} as PDF`}
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</section>
</div>
</div>
);
}
More settings and preferences blocks
- Notification preferencesChoose which alerts arrive by email, push or not at all.
- AppearanceTheme, density and accent colour.
- SecurityPassword, two-step verification and active sessions.
- Team members and rolesInvite, change roles and remove members.
- IntegrationsConnect and configure other apps.
- Delete accountA clear warning and a confirmation step.