---
title: "React-PDF alternative: a quote as a definition, not components — quario"
description: "@react-pdf/renderer builds a PDF from React components. quario renders one JSON definition to PDF, Excel and Word. The same quote with each, side by side."
url: "https://getquario.com/compare/react-pdf"
---

# React-PDF alternative

`@react-pdf/renderer` lets a React team write a PDF the way they write everything else. It’s
MIT-licensed, and it renders in Node and in the browser. You compose `Document`, `Page`, `View` and
`Text`, style them with a subset of CSS, and a flex layout engine places them. Text wraps onto new
pages, an element marked `fixed` repeats on every page, and a `render` prop can print “page 2 of 3”.
Under the components, it draws through PDFKit.

What it doesn’t have is a notion of a report. There’s no table component. A table is a `View` per
row with `flexDirection: 'row'`, and its widths are percentages you keep in step by hand. The totals,
the number formats, and the loop over the records are JavaScript in the component. That’s the part
this page compares.

## The same quote, twice

One quote, four lines, a subtotal, 21% VAT, a total, and “page 1 of 1” in the footer. First with
React-PDF 4.9 in Node:

```tsx title="quote-react-pdf.tsx"
import { Document, Page, Text, View, StyleSheet, renderToFile } from '@react-pdf/renderer'

type Line = { item: string; qty: number; unitPrice: number }
type Quote = { number: string; customer: string; validUntil: string; lines: Line[] }
type PageInfo = { pageNumber: number; totalPages: number }
const { quote } = (await getQuote(id)) as { quote: Quote }

const eur = new Intl.NumberFormat('en-IE', { style: 'currency', currency: 'EUR' })
const day = new Intl.DateTimeFormat('en-IE', { dateStyle: 'medium', timeZone: 'UTC' })
const round = (n: number) => Math.round(n * 100) / 100

const s = StyleSheet.create({
  page: { padding: 54, fontSize: 10, fontFamily: 'Helvetica' },
  title: { fontSize: 22, fontFamily: 'Helvetica-Bold' },
  meta: { flexDirection: 'row', justifyContent: 'space-between', fontSize: 9, marginTop: 6, marginBottom: 18 },
  row: { flexDirection: 'row', paddingVertical: 3 },
  head: { fontFamily: 'Helvetica-Bold', borderBottomWidth: 0.5 },
  item: { width: '52%' },
  qty: { width: '12%', textAlign: 'right' },
  money: { width: '18%', textAlign: 'right' },
  label: { width: '82%', textAlign: 'right' },
  footer: { position: 'absolute', bottom: 30, left: 54, right: 54, fontSize: 8, textAlign: 'right' },
})

function QuoteDocument({ quote }: { quote: Quote }) {
  const subtotal = quote.lines.reduce((sum, l) => sum + l.qty * l.unitPrice, 0)
  const totals: [string, number][] = [['Subtotal', subtotal], ['VAT 21%', subtotal * 0.21], ['Total', subtotal * 1.21]]
  return (
    <Document>
      <Page size="A4" style={s.page}>
        <Text style={s.title}>Quote {quote.number}</Text>
        <View style={s.meta}>
          <Text>For {quote.customer}</Text>
          <Text>Valid until {day.format(new Date(quote.validUntil))}</Text>
        </View>
        <View style={[s.row, s.head]}>
          <Text style={s.item}>Item</Text>
          <Text style={s.qty}>Qty</Text>
          <Text style={s.money}>Rate</Text>
          <Text style={s.money}>Amount</Text>
        </View>
        {quote.lines.map((l) => (
          <View key={l.item} style={s.row} wrap={false}>
            <Text style={s.item}>{l.item}</Text>
            <Text style={s.qty}>{l.qty}</Text>
            <Text style={s.money}>{eur.format(l.unitPrice)}</Text>
            <Text style={s.money}>{eur.format(round(l.qty * l.unitPrice))}</Text>
          </View>
        ))}
        <View wrap={false}>
          {totals.map(([label, amount]) => (
            <View key={label} style={s.row}>
              <Text style={s.label}>{label}</Text>
              <Text style={s.money}>{eur.format(round(amount))}</Text>
            </View>
          ))}
        </View>
        <Text style={s.footer} fixed
          render={({ pageNumber, totalPages }: PageInfo) => `Quote ${quote.number} · page ${pageNumber} of ${totalPages}`} />
      </Page>
    </Document>
  )
}

await renderToFile(<QuoteDocument quote={quote} />, 'quote.pdf')
```

That’s idiomatic React-PDF, and it renders a good quote. It’s also a component that knows
everything. The column widths live in four styles that have to add up. `wrap={false}` keeps the
totals from splitting across a page, and the header row prints once, on the first page. The amounts
are strings by the time React sees them, and the document only exists where a JSX build step runs.

Now the same quote as a quario definition. This is the file the card on the [guides page](https://getquario.com/guides.md)
renders:

```json title="quote.report.json"
{
  "data": "$.quote.lines[*]",
  "aggregates": { "subtotal": "sum:=@.qty * @.unitPrice" },
  "style": { "size": 10 },
  "header": [
    { "type": "text",
      "value": "Quote {{ $.input.quote.number }}",
      "style": { "size": 22, "bold": true } },
    { "type": "split",
      "style": { "spaceBefore": 6, "spaceAfter": 18 },
      "slots": [
        { "type": "text", "value": "For {{ $.input.quote.customer }}", "width": 60,
          "style": { "size": 9, "bold": false } },
        { "type": "text", "value": [{ "value": "Valid until " }, { "value": "{{ $.input.quote.validUntil }}" }],
          "style": { "size": 9, "bold": false, "align": "right", "format": "date" } }
      ] }
  ],
  "detail": {
    "header": { "style": { "bold": true,
      "borderBottomWidth": 0.5, "borderBottomStyle": "solid", "borderBottomColor": "#1e1f22" } },
    "columns": [
      { "header": "Item", "value": "{{ @.item }}", "width": 52 },
      { "header": { "value": "Qty", "style": { "align": "right" } }, "value": "{{ @.qty }}",
        "style": { "align": "right" }, "width": 12 },
      { "header": { "value": "Rate", "style": { "align": "right" } }, "value": "{{ @.unitPrice }}",
        "style": { "align": "right", "format": "currency" }, "width": 18 },
      { "header": { "value": "Amount", "style": { "align": "right" } }, "value": "{{ round(@.qty * @.unitPrice, 2) }}",
        "style": { "align": "right", "format": "currency" }, "width": 18 }
    ],
    "total": {
      "style": { "align": "right" },
      "rows": [
        { "cells": [
          { "value": "Subtotal", "span": 3 },
          { "value": "{{ round($.subtotal, 2) }}", "style": { "format": "currency" } }
        ] },
        { "cells": [
          { "value": "VAT 21%", "span": 3 },
          { "value": "{{ round($.subtotal * 0.21, 2) }}", "style": { "format": "currency" } }
        ] },
        { "style": { "bold": true,
            "borderTopWidth": 0.5, "borderTopStyle": "solid", "borderTopColor": "#1e1f22" },
          "cells": [
            { "value": "Total", "span": 3 },
            { "value": "{{ round($.subtotal * 1.21, 2) }}", "style": { "format": "currency" } }
          ] }
      ]
    }
  },
  "page": {
    "footer": [
      { "type": "text",
        "value": "Quote {{ $.input.quote.number }} · page {{ page.number }} of {{ page.total }}",
        "style": { "size": 8, "align": "right" } }
    ]
  }
}
```

And the code that renders it:

```ts title="quote-quario.ts"
import { writeFileSync } from 'node:fs'
import { quario } from 'quario'
import { pdf } from '@quario/pdf'
import definition from './quote.report.json' with { type: 'json' }

const report = quario({ locale: 'en-IE', currency: 'EUR' }).report(definition)

const data = await getQuote(id)                      // your JSON, from an API or DB
const bytes = await report.render(pdf({ page: { size: 'A4', margin: 54 } }), data)

writeFileSync('quote.pdf', bytes)
```

Column widths are percentages of the table, declared once per column. The subtotal is an aggregate,
and `round(…, 2)` sits on each cell that shows money. The table’s header row repeats on every page
it continues onto, and the last row keeps its totals with it, without a prop. The validity date
stays an ISO string until `"format": "date"` presents it.

## What changes when you switch

**The document becomes data.** A React-PDF document is code, and only a JSX build can run it. A
quario definition is plain JSON you store, version, and review. It can sit in a database, arrive from
an editor, or come from an AI agent, and it still renders safely. Expressions parse into closures.
There’s no `eval`, no `new Function` and no code generation, so a definition someone else wrote
can’t run anything. [How that holds →](https://getquario.com/docs/diving-deeper/security.md)

**One definition reaches four document shapes.** React-PDF writes PDF. The same quario definition
renders to an HTML fragment with `@quario/html`, a PDF with `@quario/pdf`, an Excel workbook with
`@quario/xlsx` and a Word document with `@quario/docx`:

```ts title="every-shape.ts"
import { quario } from 'quario'
import { html } from '@quario/html'
import { pdf } from '@quario/pdf'
import { xlsx } from '@quario/xlsx'
import { docx } from '@quario/docx'

const report = quario({ locale: 'en-IE', currency: 'EUR' }).report(definition)

const page = await report.render(html(), data)
const paper = await report.render(pdf({ page: { size: 'A4' } }), data)
const book = await report.render(xlsx(), data)
const word = await report.render(docx({ page: { size: 'A4' } }), data)
```

A customer who wants to edit the quote before signing gets a Word file with a real table. Their
procurement team gets a workbook whose amounts are numbers.

**React stays where it already is.** A service that renders PDF files with React-PDF carries React, a
reconciler, and a JSX build, even when it serves no UI. quario is a plain
dependency. In a React app, you can still render the definition in the browser.
[`<quario-viewer>`](https://getquario.com/docs/diving-deeper/viewer.md) then pages through it on screen with an export menu,
the job `PDFViewer` and `PDFDownloadLink` do in React-PDF.

**The bytes repeat.** The PDF target writes no creation timestamp. The same definition and data
produce a byte-identical file, so a test can compare a rendered quote against a stored one.

## When to keep React-PDF

React-PDF is the better choice when:

- **The project is open source, or the budget is zero.** React-PDF is MIT. quario is commercial past
evaluation.
- **The document is a design, not a table.** A brochure, a résumé, or a one-page layout with
flex columns and SVG drawn with its `Svg` primitives is where components shine. A report
definition is for documents whose shape comes from rows of data.
- **Your team thinks in components and the PDF is the only output.** If nobody needs the quote as
a workbook or a Word file, writing it in the language you already use is a fair trade.
- **The existing code works.** If the layout rarely changes and tests cover the components, a
rewrite buys you little.

Reach for quario when the document is data-driven, such as a quote, an invoice, a statement, or a
sales report. It earns its place when the same document has to exist as a PDF and a spreadsheet. It
also fits when someone other than your engineers changes the layout.

## FAQ

### Can I reuse my web app’s React components in either one?

Not in React-PDF as they are. Its primitives aren’t DOM elements, so a component that renders a
`div` has to be rewritten with `View` and `Text`. quario doesn’t render components at all. The
definition is the layout, and your app only hands it data.

### Do I still need to register fonts?

React-PDF ships the standard PDF families, Helvetica, Times-Roman and Courier, and `Font.register`
loads anything else. quario’s PDF target draws Latin text in the standard faces with no font files.
For full Unicode, hand `pdf({ fonts })` TrueType bytes and install the optional `fontkit` peer. The
[PDF target](https://getquario.com/docs/diving-deeper/pdf-target.md#embedding-fonts-for-full-unicode) covers it.

### Can I migrate one document at a time?

Yes. quario is a dependency, not a framework. Render the quote through a definition and leave the
rest of your React-PDF documents where they are.

### Can I use quario for free?

Evaluating it’s free, with no time limit. You get the full engine and need no key, and evaluation
output carries a watermark. Shipping it in an application or running it in your business needs a
license for each production deployment. Its users, agents, and renders are never counted, and replicas
of one service count once. [See pricing →](https://getquario.com/pricing.md)

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://getquario.com/"},{"@type":"ListItem","position":2,"name":"Comparisons","item":"https://getquario.com/compare"},{"@type":"ListItem","position":3,"name":"React-PDF alternative","item":"https://getquario.com/compare/react-pdf"}]}
```
