Escape
HomeTemplatesQuote

Quote template for JavaScript

A quote offers a fixed price for named work and holds that price until a stated date. The obligation is the offer: a validity window, no bank line, and a total rather than an amount due.

The template

Compare this to the invoice definition. The header splits, the table, and the totals block are the same shape. What changed is the date that matters, the label on the right-hand address, the word on the total, and the sentence under it:

quote.report.json
{
  "data": "$.quote.lines[*]",
  "aggregates": {
    "subtotal": "sum:[email protected] * @.unitPrice",
    "vat": "sum:[email protected] * @.unitPrice * @.vatRate / 100"
  },
  "style": { "family": "sans", "size": 10 },
  "header": [
    {
      "type": "split",
      "slots": [
        { "type": "text", "value": "Quote No. {{ $.input.quote.number }}", "width": 60,
          "style": { "family": "serif", "size": 19, "bold": true, "color": "#1e1f22" } },
        { "type": "text", "value": [{ "value": "Issued " }, { "value": "{{ $.input.quote.issued }}" }],
          "style": { "size": 8, "color": "#989a97", "align": "right", "format": "date" } }
      ]
    },
    { "type": "text", "value": [{ "value": "Valid until " }, { "value": "{{ $.input.quote.validUntil }}" }],
      "style": { "size": 8, "color": "#989a97", "align": "right", "format": "date" } },
    {
      "type": "split",
      "style": { "spaceBefore": 34 },
      "slots": [
        { "type": "text", "value": "From", "width": 50,
          "style": { "family": "mono", "size": 7, "uppercase": true, "color": "#989a97" } },
        { "type": "text", "value": "To",
          "style": { "family": "mono", "size": 7, "uppercase": true, "color": "#989a97" } }
      ]
    },
    {
      "type": "split",
      "slots": [
        { "type": "text", "value": "{{ $.input.quote.seller.name }}", "width": 50,
          "style": { "bold": true, "color": "#1e1f22" } },
        { "type": "text", "value": "{{ $.input.quote.customer.name }}",
          "style": { "bold": true, "color": "#1e1f22" } }
      ]
    },
    {
      "type": "split",
      "slots": [
        { "type": "text", "value": "{{ $.input.quote.seller.street }}", "width": 50,
          "style": { "color": "#5e6062" } },
        { "type": "text", "value": "{{ $.input.quote.customer.street }}",
          "style": { "color": "#5e6062" } }
      ]
    },
    {
      "type": "split",
      "slots": [
        { "type": "text", "value": "{{ $.input.quote.seller.city }}", "width": 50,
          "style": { "color": "#5e6062" } },
        { "type": "text", "value": "{{ $.input.quote.customer.city }}",
          "style": { "color": "#5e6062" } }
      ]
    },
    {
      "type": "split",
      "slots": [
        { "type": "text", "value": "{{ $.input.quote.seller.country }}", "width": 50,
          "style": { "color": "#5e6062" } },
        { "type": "text", "value": "{{ $.input.quote.customer.country }}",
          "style": { "color": "#5e6062" } }
      ]
    },
    {
      "type": "split",
      "style": { "spaceAfter": 40 },
      "slots": [
        { "type": "text", "value": "VAT {{ $.input.quote.seller.vatId }}", "width": 50,
          "style": { "size": 8, "color": "#989a97" } },
        { "type": "text", "value": "VAT {{ $.input.quote.customer.vatId }}",
          "style": { "size": 8, "color": "#989a97" } }
      ]
    }
  ],
  "detail": {
    "header": {
      "style": { "family": "mono", "size": 7, "uppercase": true, "color": "#989a97",
        "borderBottomWidth": 0.5, "borderBottomStyle": "solid", "borderBottomColor": "#1e1f22" }
    },
    "row": { "style": { "color": "#5e6062" } },
    "columns": [
      { "header": "Item", "value": "{{ @.item }}", "width": 46 },
      { "header": { "value": "Qty", "style": { "align": "right" } }, "value": "{{ @.qty }}", "width": 10,
        "style": { "align": "right" } },
      { "header": { "value": "Unit", "style": { "align": "right" } }, "value": "{{ @.unitPrice }}", "width": 16,
        "style": { "align": "right", "format": "currency" } },
      { "header": { "value": "VAT", "style": { "align": "right" } }, "value": "{{ @.vatRate / 100 }}", "width": 10,
        "style": { "align": "right", "color": "#989a97", "format": { "kind": "percent", "digits": 0 } } },
      { "header": { "value": "Amount", "style": { "align": "right" } }, "value": "{{ round(@.qty * @.unitPrice, 2) }}", "width": 18,
        "style": { "align": "right", "format": "currency" } }
    ],
    "total": {
      "style": { "color": "#5e6062", "align": "right", "paddingTop": 0, "paddingBottom": 0 },
      "rows": [
        {
          "cells": [
            { "value": "Subtotal", "span": 4 },
            { "value": "{{ round($.subtotal, 2) }}", "style": { "format": "currency" } }
          ]
        },
        {
          "cells": [
            { "value": "VAT 21%", "span": 4 },
            { "value": "{{ round($.vat, 2) }}", "style": { "format": "currency" } }
          ]
        },
        {
          "style": { "bold": true, "color": "#1e1f22", "paddingTop": 5,
            "borderTopWidth": 0.5, "borderTopStyle": "solid", "borderTopColor": "#1e1f22" },
          "cells": [
            { "value": "Total", "span": 4 },
            { "value": "{{ round($.subtotal + $.vat, 2) }}", "style": { "format": "currency" } }
          ]
        }
      ]
    }
  },
  "page": {
    "footer": [
      {
        "type": "text",
        "value": [{ "value": "This quote is binding until " }, { "value": "{{ $.input.quote.validUntil }}" }, { "value": ". It is not a tax invoice." }],
        "style": { "size": 8, "color": "#989a97", "paddingTop": 10, "format": "date",
          "borderTopWidth": 0.5, "borderTopStyle": "solid", "borderTopColor": "#1e1f22" }
      }
    ]
  }
}

The totals are detail.total, a block of rows in the table itself, so each figure lands in the column it belongs to. Each label is one cell with a span covering the columns it reaches. What the rows share is the block’s own style. The rule above Total is a borderTop declared on that row alone, drawn continuously across the width. The fine print sits in page.footer, so it’s drawn on the page’s baseline rather than wherever the totals end. That’s what lets a short quote still read as a full page. A definition declares amounts rather than formatting them. The cell interpolates the bare number and "format": "currency" says how to show it, from the locale and currency you hand quario(). The total wraps its one sum in round(…, 2), so the workbook stores 2676.52 rather than whatever float the addition landed on. Dates work the same way. The Issued and Valid until lines, and the sentence in the page footer, use styled runs — a value written as a list. Each date stands in a run of its own, and the line’s "format": "date" presents it as 9 Jul 2026 under en-IE. The kind leaves the words around it alone. The template registers no function for it.

The data

This is the sample the preview below renders. $.quote.lines[*] selects the rows the table repeats over, and $.input reads the parties, the number, and the two dates:

quote.data.json
{
  "quote": {
    "number": "Q-2026-0194",
    "issued": "2026-07-09",
    "validUntil": "2026-08-08",
    "seller": {
      "name": "Quire Press Ltd.",
      "street": "Binnenkant 42",
      "city": "1011 BM Amsterdam",
      "country": "Netherlands",
      "vatId": "NL8123.45.678.B01"
    },
    "customer": {
      "name": "Folio & Sons",
      "street": "12 Marlborough Street",
      "city": "Dublin 1",
      "country": "Ireland",
      "vatId": "IE6388047V"
    },
    "lines": [
      { "item": "Page design", "qty": 8, "unitPrice": 95, "vatRate": 21 },
      { "item": "Plate making", "qty": 16, "unitPrice": 22, "vatRate": 21 },
      { "item": "Offset printing, 500 copies", "qty": 500, "unitPrice": 1.85, "vatRate": 21 },
      { "item": "Saddle stitching", "qty": 500, "unitPrice": 0.35, "vatRate": 21 }
    ]
  }
}

What it renders

This isn’t a screenshot. quario compiles the definition and data above, and @quario/viewer renders them in your browser, right here. The PDF button hands you the same quote as real @quario/pdf bytes, one A4 page, no browser rendering involved. The DOCX button beside it hands the same definition to @quario/docx, where Word paginates and resolves the faces itself:

Rendering live with @quario/viewerA4

Use it

shell
npm install quario @quario/html
app.js
import { quario } from 'quario'
import { html } from '@quario/html'
import quote from './quote.report.json' with { type: 'json' }

const q = quario({ locale: 'en-IE', currency: 'EUR' })   // what `format` presents through
const report = q.report(quote)

const data = await getQuote(id)                      // your data
const page = await report.render(html(), data)       // ready to display or print

Show page in the browser, or render the same compiled report straight to a PDF to send and keep for the file — no browser involved:

pdf.js
import { writeFileSync } from 'node:fs'
import { pdf } from '@quario/pdf'

const bytes = await report.render(pdf({ page: { size: 'A4', margin: 54 } }), data)

writeFileSync('quote.pdf', bytes)

See JSON to PDF for both PDF routes side by side.

Make it yours

The validity window is a date in your data. A second VAT rate is a field on the line and another aggregate, the way the receipt splits 9% from 21%. Drop the VAT column for a zero-rated customer. Localise the labels. None of that’s layout work. It’s a JSON file your reviewers can read in a pull request. The same definition renders identically in Node, the browser, and at the edge. It renders without eval, so customer-supplied item names never become code. Security →

FAQ

Can I generate the quote as a PDF?

Yes, and it’s usually the format you send: @quario/pdf renders this definition straight to bytes you can attach, with no browser anywhere in the path. That matters when a background job raises the quote rather than someone sitting at a screen. Full example →

Can I export the quote to Excel?

Yes, and a recipient who wants to re-price a line list wants cells, not a PDF. @quario/xlsx renders this same definition to a workbook, with nothing to change. The amount columns declare "format": "currency" rather than baking a formatted string into the cell. Each one arrives as a real number the recipient can re-price and total. Full example →

Can I use this quote template commercially?

The report definitions, data, and output you create with quario belong to you. quario is free to evaluate with no time limit. Sending priced offers to real customers is past evaluation, so that needs a seat for each developer working with it. Deployment is then unlimited and royalty-free. Pricing →

Is this a tax invoice?

No. A quote is an offer for a stated period. VAT on the lines doesn’t make it an invoice. It has no due date and no payment rail. That’s why the gallery keeps two definitions.

Where are the other templates?

Browse the template gallery → — invoices, receipts, purchase orders, and more.

Fold your data into this quote.

npm install quario
Getting started
© 2026 quario · KvK 61815977