← Templates<Document>
Pell & Ashby invoice
An architecture practice's invoice, and the hours behind it as charts on its second page.
Document 666px: background #ffffff
Document.Page 666px
Row 666px: 2 side by side, stacked on phones
Column 321px
Text 321px "Pell & Ashby"
Text 321px "Architects"
Column 321px
Text 321px "14 Calvert Mews, London E2 7LL / +44 20 7946 0321 · studio@pellashby.example"
Row 666px: 2 side by side, stacked on phones
Column 322px
Heading h1 322px "Invoice"
Text 322px "Hawthorn Lane House, Stage 4 / 1 September to 31 October 2026"
Column 320px
Column 320px
Text 320px "Billed to"
Text 320px "Clara Wendt, Wendt Family Trust / 3 Hawthorn Lane, Saffron Walden CB11 4QE"
Column 666px
Row 666px: 4 side by side, stacked on phones
Column 149px
Column 149px
Text 149px "Invoice"
Text 149px "PA-0114"
Column 149px
Column 149px
Text 149px "Issued"
Text 149px "4 November 2026"
Column 148px
Column 148px
Text 148px "Due"
Text 148px "4 December 2026"
Column 148px
Column 148px
Text 148px "Amount due"
Text 148px "£17,731.20"
Table 666px
TableRow 666px
TableCell 394px "Work"
TableCell 64px "Hours"
TableCell 72px "Rate"
TableCell 136px "Amount"
TableRow 666px
TableCell 394px "Technical design drawings / Plans, sections and elevations at 1:50; key details…"
TableCell 64px "64"
TableCell 72px "£95"
TableCell 136px "6,080.00"
TableRow 666px
TableCell 394px "Specification and schedules / Doors, windows, finishes and ironmongery"
TableCell 64px "36"
TableCell 72px "£85"
TableCell 136px "3,060.00"
TableRow 666px
TableCell 394px "Structural coordination / With Hale Engineering: steel frame and foundations"
TableCell 64px "22"
TableCell 72px "£95"
TableCell 136px "2,090.00"
TableRow 666px
TableCell 394px "Planning conditions / Materials, landscaping and drainage: conditions 3, 5 and 9"
TableCell 64px "18"
TableCell 72px "£95"
TableCell 136px "1,710.00"
TableRow 666px
TableCell 394px "Site visits and minutes / Four visits, 2 September to 28 October"
TableCell 64px "16"
TableCell 72px "£95"
TableCell 136px "1,520.00"
TableRow 666px
TableCell 394px "Expenses, at cost / Printing and rail travel; receipts attached"
TableCell 64px "—"
TableCell 72px "—"
TableCell 136px "316.00"
TableRow 666px
TableCell 394px
TableCell 136px "Subtotal"
TableCell 136px "14,776.00"
TableRow 666px
TableCell 394px
TableCell 136px "VAT at 20%"
TableCell 136px "2,955.20"
TableRow 666px
TableCell 394px
TableCell 136px "Total due"
TableCell 136px "£17,731.20"
Column 666px
Row 666px: 4 side by side, stacked on phones
Column 149px
Column 149px
Text 149px "Pay by transfer"
Text 149px "Within 30 days, please"
Column 149px
Column 149px
Text 149px "Account name"
Text 149px "Pell & Ashby Ltd"
Column 148px
Column 148px
Text 148px "Sort code · Account"
Text 148px "20-45-77 · 63418820"
Column 148px
Column 148px
Text 148px "Reference"
Text 148px "PA-0114"
Document.Page 666px
Row 666px: 2 side by side, stacked on phones
Column 321px
Text 321px "Pell & Ashby"
Column 321px
Text 321px "Invoice PA-0114 · Time summary"
Column 666px
Heading h2 666px "Where the hours went"
Text 520px "Stage 4 took 156 hours between 1 September and 31 October. Here they are by pha…"
Column 666px
Text 666px "By phase"
Column 666px
Row 666px: 3 side by side, stacked on phones
Column 196px
Text 196px "Technical design drawings"
Svg 325px (1 shapes)
Column 96px
Text 96px "64 h · 41%"
Row 666px: 3 side by side, stacked on phones
Column 196px
Text 196px "Specification and schedules"
Svg 325px (1 shapes)
Column 96px
Text 96px "36 h · 23%"
Row 666px: 3 side by side, stacked on phones
Column 196px
Text 196px "Structural coordination"
Svg 325px (1 shapes)
Column 96px
Text 96px "22 h · 14%"
Row 666px: 3 side by side, stacked on phones
Column 196px
Text 196px "Planning conditions"
Svg 325px (1 shapes)
Column 96px
Text 96px "18 h · 12%"
Row 666px: 3 side by side, stacked on phones
Column 196px
Text 196px "Site visits and minutes"
Svg 325px (1 shapes)
Column 96px
Text 96px "16 h · 10%"
Column 666px
Text 666px "By week"
Column 666px
Svg 665px (10 shapes)
Column 666px
Row 666px: 9 side by side, stacked on phones
Column 74px
Text 74px "14 h / 31 Aug"
Column 74px
Text 74px "18 h / 7 Sep"
Column 74px
Text 74px "22 h / 14 Sep"
Column 74px
Text 74px "20 h / 21 Sep"
Column 74px
Text 74px "16 h / 28 Sep"
Column 74px
Text 74px "12 h / 5 Oct"
Column 74px
Text 74px "19 h / 12 Oct"
Column 74px
Text 74px "21 h / 19 Oct"
Column 74px
Text 74px "14 h / 26 Oct"
Column 666px
Text 666px "By person"
Table 666px
TableRow 666px
TableCell 345px "Ines Morrow"
TableCell 193px "Project architect"
TableCell 64px "74 h"
TableCell 64px "47%"
TableRow 666px
TableCell 345px "Sam Okafor"
TableCell 193px "Architectural assistant"
TableCell 64px "34 h"
TableCell 64px "22%"
TableRow 666px
TableCell 345px "Hannah Pell"
TableCell 193px "Director"
TableCell 64px "28 h"
TableCell 64px "18%"
TableRow 666px
TableCell 345px "Oliver Ashby"
TableCell 193px "Director"
TableCell 64px "20 h"
TableCell 64px "13%"
Text 480px "Hours are recorded to the quarter hour and rounded here, so the phases add up t…"
PageFooter 666px
Row 666px: 2 side by side, stacked on phones
Column 578px
Text 578px "Pell & Ashby Architects Ltd · Registered in England and Wales, 09412873 · VAT G…"
Column 64px
Text 64px "of"- No diagnostics. Nothing to fix, nothing degraded.
/**
* Pell & Ashby (a fictional London architecture practice) invoices a client for a stage of work,
* with the hours behind it on a second page.
*
* A printed page, so white paper, black ink and one bottle green, kept for the amount owed and
* the charts. Newsreader for the words and every figure (its figures are tabular, so a column of
* amounts lines up), Hanken Grotesk in small capitals for the labels. Everything hangs from four
* columns: the dates, the addresses and the table's figures all start on them.
*/
import type { ReactNode } from "react";
import { Column, Document, Grid, Heading, LineBreak, PageFooter, PageNumber, Text, Rect, Row, Span, Svg, Table, TableCell, TableRow } from "@hyper-elements/react";
const INK = "#1C1B19";
const QUIET = "#6B675F";
const RULE = "#DAD6CD";
const GREEN = "#24513D";
const SAGE = "#B9C7BC";
const SERIF = "Newsreader, Georgia, 'Times New Roman', serif";
const SANS = "'Hanken Grotesk', 'Helvetica Neue', Helvetica, Arial, sans-serif";
/** A4 at 96 px to the inch, less the side margins: the width everything lays out in. */
const WIDTH = 665;
const GAP = 24;
/** Two of the four columns and the gap between them. */
const HALF = (WIDTH - GAP) / 2;
/** Small capitals in the sans: every label. */
const label = { fontFamily: SANS, fontSize: 9.5, fontWeight: 600, lineHeight: 1.4, letterSpacing: 1.1, textTransform: "uppercase", color: QUIET } as const;
/** A label over a value, the dates' and the addresses' unit. */
const field = (name: string, value: ReactNode, accent = false) => (
<Column style={{ gap: 6 }}>
<Text style={label}>{name}</Text>
<Text style={{ fontSize: 14, lineHeight: 1.45, color: accent ? GREEN : INK, fontWeight: accent ? 600 : 400 }}>{value}</Text>
</Column>
);
const hairline = (color = RULE) => `1px solid ${color}`;
/** A line of the invoice: what was done, a note under it, and its hours, rate and amount. */
const line = (what: string, note: string, hours: string, rate: string, amount: string) => (
<TableRow>
<TableCell style={{ padding: [10, 16, 11, 0], border: { bottom: hairline() } }}>
{what}
<LineBreak />
<Span style={{ fontFamily: SANS, fontSize: 10.5, color: QUIET }}>{note}</Span>
</TableCell>
<TableCell style={{ width: 64, padding: [10, 0, 11], textAlign: "right", verticalAlign: "top", border: { bottom: hairline() } }}>{hours}</TableCell>
<TableCell style={{ width: 72, padding: [10, 0, 11], textAlign: "right", verticalAlign: "top", border: { bottom: hairline() } }}>{rate}</TableCell>
<TableCell style={{ width: 136, padding: [10, 0, 11], textAlign: "right", verticalAlign: "top", border: { bottom: hairline() } }}>{amount}</TableCell>
</TableRow>
);
/** A line of the totals, under the hours and rate columns. */
const total = (name: string, amount: string, last = false) => (
<TableRow>
<TableCell />
<TableCell colSpan={2} style={{ padding: last ? [12, 0, 1] : [8, 0], border: last ? { top: hairline(INK) } : undefined, verticalAlign: "bottom", fontSize: last ? 14 : 13.5, color: last ? INK : QUIET }}>
{name}
</TableCell>
<TableCell style={{ padding: last ? [10, 0, 0] : [8, 0], border: last ? { top: hairline(INK) } : undefined, textAlign: "right", verticalAlign: "bottom", fontSize: last ? 26 : 14, lineHeight: last ? 1.1 : 1.45, letterSpacing: last ? -0.3 : 0, color: last ? GREEN : INK }}>
{amount}
</TableCell>
</TableRow>
);
/** A section of the time summary: its label over a rule. */
const part = (name: string, children: ReactNode) => (
<Column style={{ gap: 16 }}>
<Text style={{ ...label, color: INK, padding: [0, 0, 8], border: { bottom: hairline(INK) } }}>{name}</Text>
{children}
</Column>
);
/** Hours by phase, as horizontal bars beside their names. */
const PHASES: Array<[string, number]> = [
["Technical design drawings", 64],
["Specification and schedules", 36],
["Structural coordination", 22],
["Planning conditions", 18],
["Site visits and minutes", 16],
];
const HOURS = 156;
const BAR = WIDTH - 196 - 96 - 2 * GAP;
const phase = ([name, hours]: [string, number]) => (
<Row key={name} style={{ alignItems: "center", gap: GAP }}>
<Column style={{ width: 196 }}>
<Text style={{ fontSize: 13.5 }}>{name}</Text>
</Column>
<Svg width={BAR} height={10} title={`${hours} hours`}>
<Rect width={Math.round((hours / 64) * BAR)} height={10} style={{ fill: hours === 64 ? GREEN : SAGE }} />
</Svg>
<Column style={{ width: 96 }}>
<Text style={{ fontSize: 13.5, textAlign: "right" }}>
{hours} h <Span style={{ color: QUIET }}>· {Math.round((hours / HOURS) * 100)}%</Span>
</Text>
</Column>
</Row>
);
/** Hours by week, as columns over the weeks' Mondays: drawn in one Svg, its labels in a Grid of as many columns. */
const WEEKS: Array<[string, number]> = [
["31 Aug", 14],
["7 Sep", 18],
["14 Sep", 22],
["21 Sep", 20],
["28 Sep", 16],
["5 Oct", 12],
["12 Oct", 19],
["19 Oct", 21],
["26 Oct", 14],
];
const CHART = 120;
const slot = WIDTH / WEEKS.length;
const weeks = (
<Column style={{ gap: 10 }}>
<Svg width={WIDTH} height={CHART + 1} title="Hours by week, from 14 to 22">
{WEEKS.map(([week, hours], i) => {
const height = Math.round((hours / 22) * (CHART - 8));
return <Rect key={week} x={Math.round(i * slot + slot / 2 - 18)} y={CHART - height} width={36} height={height} style={{ fill: hours === 22 ? GREEN : SAGE }} />;
})}
<Rect x={0} y={CHART} width={WIDTH} height={1} style={{ fill: INK }} />
</Svg>
<Grid columns={WEEKS.length} style={{ gap: 0 }}>
{WEEKS.map(([week, hours]) => (
<Text key={week} style={{ textAlign: "center", fontSize: 13.5, lineHeight: 1.35 }}>
{hours} h
<LineBreak />
<Span style={{ fontFamily: SANS, fontSize: 10, color: QUIET }}>{week}</Span>
</Text>
))}
</Grid>
</Column>
);
/** A row of the people table. */
const person = (name: string, role: string, hours: number) => (
<TableRow>
<TableCell style={{ width: HALF + GAP }}>{name}</TableCell>
<TableCell style={{ color: QUIET }}>{role}</TableCell>
<TableCell style={{ width: 64, textAlign: "right" }}>{hours} h</TableCell>
<TableCell style={{ width: 64, textAlign: "right", color: QUIET }}>{Math.round((hours / HOURS) * 100)}%</TableCell>
</TableRow>
);
export default function PellAshbyInvoice() {
return (
<Document
size="A4"
margin={[56, 64, 44, 64]}
title="Invoice PA-0114, Pell & Ashby"
lang="en-GB"
fonts={[
{ family: "Newsreader", src: "https://fonts.gstatic.com/s/newsreader/v26/cY9AfjOCX1hbuyalUrK4397yjIJFJpc.woff2", format: "woff2", weight: "200 800" },
{ family: "Newsreader", src: "https://fonts.gstatic.com/s/newsreader/v26/cY9CfjOCX1hbuyalUrK439vCjohCBJWxZA.woff2", format: "woff2", weight: "200 800", style: "italic" },
{ family: "Hanken Grotesk", src: "https://fonts.gstatic.com/s/hankengrotesk/v12/ieVn2YZDLWuGJpnzaiwFXS9tYtpd59CxCis4.woff2", format: "woff2", weight: "100 900" },
]}
style={{ backgroundColor: "#ffffff", color: INK, fontFamily: SERIF, fontSize: 14, lineHeight: 1.45 }}
>
<Document.Page style={{ gap: 40 }}>
{/* Letterhead */}
<Row style={{ alignItems: "start", gap: GAP }}>
<Column style={{ gap: 4 }}>
<Text style={{ fontSize: 25, fontWeight: 500, letterSpacing: -0.5, lineHeight: 1 }}>Pell & Ashby</Text>
<Text style={label}>Architects</Text>
</Column>
<Column>
<Text style={{ fontFamily: SANS, fontSize: 10.5, lineHeight: 1.6, color: QUIET, textAlign: "right" }}>
14 Calvert Mews, London E2 7LL
<LineBreak />
+44 20 7946 0321 · studio@pellashby.example
</Text>
</Column>
</Row>
{/* Title, and who it's to: the address starts on the third of four columns */}
<Row style={{ alignItems: "end", gap: GAP }}>
<Column style={{ gap: 12 }}>
<Heading level={1} style={{ fontSize: 84, fontWeight: 300, lineHeight: 0.95, letterSpacing: -2.5 }}>
Invoice
</Heading>
<Text style={{ fontSize: 18, fontStyle: "italic", color: QUIET, lineHeight: 1.35 }}>
Hawthorn Lane House, Stage 4
<LineBreak />1 September to 31 October 2026
</Text>
</Column>
<Column style={{ width: HALF }}>
{field(
"Billed to",
<>
Clara Wendt, Wendt Family Trust
<LineBreak />3 Hawthorn Lane, Saffron Walden CB11 4QE
</>
)}
</Column>
</Row>
{/* The number, the dates and the amount, on the four columns */}
<Grid columns={4} style={{ gap: GAP, padding: [12, 0, 0], border: { top: hairline(INK) } }}>
{field("Invoice", "PA-0114")}
{field("Issued", "4 November 2026")}
{field("Due", "4 December 2026")}
{field("Amount due", "£17,731.20", true)}
</Grid>
{/* The work, and what it comes to */}
<Table style={{ fontSize: 14, lineHeight: 1.45 }}>
<TableRow header>
<TableCell style={{ ...label, padding: [0, 0, 8], border: { bottom: hairline(INK) } }}>Work</TableCell>
<TableCell style={{ ...label, padding: [0, 0, 8], textAlign: "right", border: { bottom: hairline(INK) } }}>Hours</TableCell>
<TableCell style={{ ...label, padding: [0, 0, 8], textAlign: "right", border: { bottom: hairline(INK) } }}>Rate</TableCell>
<TableCell style={{ ...label, padding: [0, 0, 8], textAlign: "right", border: { bottom: hairline(INK) } }}>Amount</TableCell>
</TableRow>
{line("Technical design drawings", "Plans, sections and elevations at 1:50; key details at 1:5", "64", "£95", "6,080.00")}
{line("Specification and schedules", "Doors, windows, finishes and ironmongery", "36", "£85", "3,060.00")}
{line("Structural coordination", "With Hale Engineering: steel frame and foundations", "22", "£95", "2,090.00")}
{line("Planning conditions", "Materials, landscaping and drainage: conditions 3, 5 and 9", "18", "£95", "1,710.00")}
{line("Site visits and minutes", "Four visits, 2 September to 28 October", "16", "£95", "1,520.00")}
{line("Expenses, at cost", "Printing and rail travel; receipts attached", "—", "—", "316.00")}
{total("Subtotal", "14,776.00")}
{total("VAT at 20%", "2,955.20")}
{total("Total due", "£17,731.20", true)}
</Table>
{/* How to pay, on the same four columns */}
<Grid columns={4} style={{ gap: GAP, padding: [12, 0, 0], border: { top: hairline(INK) } }}>
{field("Pay by transfer", "Within 30 days, please")}
{field("Account name", "Pell & Ashby Ltd")}
{field("Sort code · Account", "20-45-77 · 63418820")}
{field("Reference", "PA-0114")}
</Grid>
</Document.Page>
<Document.Page style={{ gap: 36 }}>
<Row style={{ alignItems: "end", gap: GAP, padding: [0, 0, 12], border: { bottom: hairline() } }}>
<Column>
<Text style={{ fontSize: 17, fontWeight: 500, letterSpacing: -0.3, lineHeight: 1 }}>Pell & Ashby</Text>
</Column>
<Column>
<Text style={{ ...label, textAlign: "right" }}>Invoice PA-0114 · Time summary</Text>
</Column>
</Row>
<Column style={{ gap: 12 }}>
<Heading level={2} style={{ fontSize: 44, fontWeight: 300, lineHeight: 1.05, letterSpacing: -1.1 }}>
Where the hours went
</Heading>
<Text style={{ fontSize: 17, fontStyle: "italic", lineHeight: 1.45, color: QUIET, maxWidth: 520 }}>
Stage 4 took 156 hours between 1 September and 31 October. Here they are by phase, by week and by the people who worked them.
</Text>
</Column>
{part("By phase", <Column style={{ gap: 14 }}>{PHASES.map(phase)}</Column>)}
{part("By week", weeks)}
{part(
"By person",
<Table style={{ fontSize: 14, lineHeight: 1.45, cellPadding: [9, 0], rowBorder: hairline() }}>
{person("Ines Morrow", "Project architect", 74)}
{person("Sam Okafor", "Architectural assistant", 34)}
{person("Hannah Pell", "Director", 28)}
{person("Oliver Ashby", "Director", 20)}
</Table>
)}
<Text style={{ fontFamily: SANS, fontSize: 10.5, lineHeight: 1.6, color: QUIET, maxWidth: 480 }}>
Hours are recorded to the quarter hour and rounded here, so the phases add up to the total. The full timesheet is yours on request.
</Text>
</Document.Page>
<PageFooter style={{ padding: [12, 0, 0], border: { top: hairline() } }}>
<Row style={{ gap: GAP }}>
<Column>
<Text style={{ fontFamily: SANS, fontSize: 9.5, color: QUIET }}>Pell & Ashby Architects Ltd · Registered in England and Wales, 09412873 · VAT GB 318 4471 02</Text>
</Column>
<Column style={{ width: 64 }}>
<Text style={{ fontFamily: SANS, fontSize: 9.5, color: QUIET, textAlign: "right" }}>
<PageNumber /> of <PageNumber total />
</Text>
</Column>
</Row>
</PageFooter>
</Document>
);
}import { renderPdf } from "@hyper-elements/react/pdf";
import PellAshbyInvoice from "./pell-ashby-invoice";
const { pdf } = await renderPdf(<PellAshbyInvoice />);