The Invoice Is an App
An invoice is three computations: Σ(price × quantity), a tax rate applied to the subtotal, and a sum. Mere builds one in under 90 lines of declarative HTML — reactive computed chain, two screens, no JavaScript. The document and the app were always the same thing.
The Invoice Problem
An invoice seems like a document. It has a sender, a recipient, a date, a list of line items, and a total. You print it. You email it as a PDF. It is a record, not a program.
But every invoice you have ever seen was actually computed. Someone opened a spreadsheet, typed quantities and prices, and the subtotal calculated itself. The tax was a formula. The balance due was a sum. The “document” was quietly a running program the whole time — one that required Excel or Google Sheets to host it, and surrendered the user's data to a cloud provider the moment they clicked “save to Drive.”
The invoice was always an app. Mere just makes that explicit.
What an Invoice Actually Is
Strip it to primitives. An invoice is:
State
Named values — sender, recipient, line items, tax rate, dates. Editable. Persistent across sessions.
Computed state
Values derived from state — subtotal (sum of price × qty), tax amount (subtotal × rate), balance (subtotal + tax). Reactive: change a quantity, the balance updates instantly.
Two views
A presentation screen (the invoice as it would appear to the client) and an edit screen (fields and a spreadsheet for line items). Same state, two renderings.
That is the complete structure. Mere encodes it in markup — no JavaScript, no framework, no build step.
Three Lines of Computed State
The financial logic of any invoice is three dependent computations. In Mere, that is three lines in a <computed> block:
<computed>
<value name="subtotal" from="items" op="sum-product" field="price" by="qty" />
<value name="tax-amount" from="subtotal,tax-rate" op="percent-of" />
<value name="balance" from="subtotal,tax-amount" op="add" />
</computed>Three operations: sum-product, percent-of, add.
sum-productΣ(price × qty)Iterates the items list and sums the product of two fields per row. No stored per-row amounts — the total derives from the raw data.
percent-ofa × (b / 100)Applied to subtotal and tax-rate. Change the tax rate field, tax-amount recomputes immediately.
adda + bSubtotal plus tax-amount. The cascade is automatic — editing a line item quantity propagates through all three values.
The cascade is deterministic. Edit quantity in row 2 → subtotal updates → tax-amount updates → balance updates. All three in a single reactive pass. No event handlers. No useEffect chains.
The Line Items Table
The invoice screen shows a read-only data table. Each row computes its own amount column — price × quantity — without storing that value in state:
<data-table @items>
<column field="description" label="Description" />
<column field="price" label="Price" as="currency" />
<column field="qty" label="Qty" />
<column field="price" label="Amount" as="product" by="qty" />
</data-table>The as="product" column type is a render-time computation: it takes the field and multiplies it by by per row, formats as currency, and displays the result. Nothing is stored. The source of truth for every amount is always the raw price and quantity in state.
On the edit screen, the same list renders as a <spreadsheet> — an inline editable grid with the same columns. Click a cell, type a new value, hit enter. The computed chain responds immediately in both directions: edit any quantity in the spreadsheet, and the totals on the invoice screen are already updated.
The Totals Block
Totals render as <kv> rows — a label on the left, a formatted value on the right. Each one reads a computed value and formats it:
<heading>Total</heading>
<kv @subtotal format="currency" label="Subtotal" />
<kv @tax-rate format="percent" label="Tax rate" />
<kv @tax-amount format="currency" label="Tax" />
<kv @balance format="currency" label="Balance due" />The @ sigil binds each <kv> to a state or computed value. Change the tax rate in the edit screen: the “Tax rate” row updates, the “Tax” row updates, the “Balance due” row updates — all three in the same reactive pass, with no JavaScript written to wire them together.
Two Screens, One File
The invoice has two screens: one for viewing, one for editing. They share all the same state — there is no synchronization, no copy, no save button. The screens are just two different windows into the same declared values.
<navigation-bar "bottom">
<nav-item "invoice">Invoice</nav-item>
<nav-item "edit">Edit</nav-item>
</navigation-bar>The <nav-item> takes the target screen name as its content argument. No action, no handler, no routing configuration. Switch screens, and all the @-bound elements re-read from the same state they were always reading from. Everything is consistent by default.
Persistence is one attribute. Add persist to any state value and it survives browser closes — written to OPFS with a localStorage fallback. Every field in the invoice persists: the sender details, the line items, the invoice number, the tax rate, everything. Close the browser, reopen it, and the invoice is exactly where you left it. No server. No account. No sync.
The Live Workbook
Below is the invoice running in your browser right now. Switch to the Edit tab, change a quantity or add a line item. The totals on the Invoice tab update in real time. This is the complete workbook — one file, no JavaScript, no dependencies.
The Complete Workbook
The full source — 89 lines of declarative HTML. State, computed chain, two screens, navigation, persistence. Everything:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Invoice — Mere</title>
<script src="/mere-runtime.js"></script>
</head>
<body>
<workbook theme="classic-light">
<state>
<value name="from-name" type="text" value="Semantic Intent" persist />
<value name="from-address" type="text" value="Brooklyn, New York" persist />
<value name="from-email" type="text" value="[email protected]" persist />
<value name="to-name" type="text" value="StratIQX" persist />
<value name="to-address" type="text" value="Strategic Consulting Division" persist />
<value name="invoice-number" type="text" value="INV-0042" persist />
<value name="invoice-date" type="text" value="Apr 17, 2026" persist />
<value name="due-date" type="text" value="May 17, 2026" persist />
<value name="tax-rate" type="number" value="8" persist />
<value name="notes" type="text"
value="Payment due within 30 days. Bank transfer or PayPal accepted." persist />
<value name="items" type="list" value='[
{"id":"1","description":"Strategy workshop","price":3200,"qty":1},
{"id":"2","description":"Cascade analysis (hourly)","price":145,"qty":12},
{"id":"3","description":"Intelligence framework","price":1800,"qty":1},
{"id":"4","description":"Reporting & handoff","price":95,"qty":4}
]' persist />
</state>
<computed>
<value name="subtotal" from="items" op="sum-product" field="price" by="qty" />
<value name="tax-amount" from="subtotal,tax-rate" op="percent-of" />
<value name="balance" from="subtotal,tax-amount" op="add" />
</computed>
<screen name="invoice">
<header>
<heading>Invoice</heading>
<subtitle>@invoice-number</subtitle>
</header>
<heading>@from-name</heading>
<paragraph>@from-address</paragraph>
<paragraph>@from-email</paragraph>
<heading>Bill to</heading>
<paragraph>@to-name</paragraph>
<paragraph>@to-address</paragraph>
<kv @invoice-number label="Invoice #" />
<kv @invoice-date label="Date" />
<kv @due-date label="Due" />
<heading>Line items</heading>
<data-table @items>
<column field="description" label="Description" />
<column field="price" label="Price" as="currency" />
<column field="qty" label="Qty" />
<column field="price" label="Amount" as="product" by="qty" />
</data-table>
<heading>Total</heading>
<kv @subtotal format="currency" label="Subtotal" />
<kv @tax-rate format="percent" label="Tax rate" />
<kv @tax-amount format="currency" label="Tax" />
<kv @balance format="currency" label="Balance due" />
<paragraph>@notes</paragraph>
<navigation-bar "bottom">
<nav-item "invoice">Invoice</nav-item>
<nav-item "edit">Edit</nav-item>
</navigation-bar>
</screen>
<screen name="edit">
<header><heading>Edit</heading></header>
<heading>From</heading>
<field ~from-name placeholder="Your name" />
<field ~from-address placeholder="Address" />
<field ~from-email placeholder="Email" />
<heading>Bill to</heading>
<field ~to-name placeholder="Client name" />
<field ~to-address placeholder="Client address" />
<heading>Invoice</heading>
<field ~invoice-number placeholder="Invoice #" />
<field ~invoice-date placeholder="Invoice date" />
<field ~due-date placeholder="Due date" />
<field ~tax-rate placeholder="Tax rate %" type="number" />
<heading>Line items</heading>
<spreadsheet @items>
<column field="description" label="Description" editable />
<column field="price" label="Price" editable format="currency" />
<column field="qty" label="Qty" editable format="number" />
</spreadsheet>
<heading>Notes</heading>
<field ~notes placeholder="Payment terms, notes…" />
<navigation-bar "bottom">
<nav-item "invoice">Invoice</nav-item>
<nav-item "edit">Edit</nav-item>
</navigation-bar>
</screen>
</workbook>
</body>
</html>The Boundary That Was Never Real
The separation between “document” and “application” was always a boundary imposed by tooling, not by the nature of the things themselves. A spreadsheet is a document that computes. An invoice is a document that reacts. A form is a document that captures.
Every time we built these things as web apps — with servers and databases and JavaScript bundles and auth flows — we were solving a hosting problem that only existed because we chose a format that required hosting.
Mere does not solve the invoice problem by building a better invoice app. It solves it by making the file reactive. The invoice is a file. The file runs. You send it. The recipient opens it. Their changes stay on their machine. The data is theirs.
The document and the app were always the same thing. It just took a different format to make that visible.
Try it: open the workbook above and edit a line item quantity.
Watch the subtotal, tax, and balance update in real time — no JavaScript written, no event handlers, no framework. Then open the raw file and read the source. Everything you see is declared, not programmed.