Escape
HomeGuidesHTML reports

HTML reports, in JavaScript

You have rows — orders, accounts, a month of movements — and you need them on a page as a report a person can read. That means sections with their own subtotals, a table under each, and a total at the end. The part worth knowing before you start is what quario hands back: a fragment. It’s report markup with no <html> around it, and it goes inside the page you already have.

The approach in one line

Rows + a report definition → a fragment you place. quario writes the report’s markup and nothing around it. No <html>, no <head>, no stylesheet link, no opinion about the rest of your page.

Do it with quario

Install:

shell
npm install @quario/html

Describe the report once. groups is what makes it sectioned. Each group opens a band of its own and carries its own aggregates. A subtotal per region is then a declaration, not a second pass over the data:

revenue.report.json
{
  "data": "$.sales[*]",
  "aggregates": { "total": "sum:[email protected]" },
  "header": [
    { "type": "text",
      "value": "Revenue by region",
      "style": { "size": 20, "bold": true, "spaceAfter": 10 } }
  ],
  "groups": [
    { "name": "region",
      "by": "[email protected]",
      "aggregates": { "subtotal": "sum:[email protected]" },
      "header": [
        { "type": "text",
          "value": "{{ region.key }}",
          "style": { "size": 13, "bold": true, "spaceBefore": 14, "spaceAfter": 4 } }
      ] }
  ],
  "detail": {
    "columns": [
      { "header": "Account", "value": "{{ @.account }}", "width": 70 },
      { "header": { "value": "Amount", "style": { "align": "right" } },
        "value": "{{ @.amount }}",
        "style": { "align": "right", "format": "currency" }, "width": 30 }
    ],
    "total": {
      "style": { "align": "right", "bold": true },
      "rows": [
        { "cells": [
          { "value": "Subtotal {{ region.key }}" },
          { "value": "{{ round(region.subtotal, 2) }}", "style": { "format": "currency" } }
        ] }
      ]
    }
  }
}

by is the expression that groups the rows. region is the handle the bands read through — {{ region.key }} for the value grouped on, {{ region.subtotal }} for the aggregate declared beside it. The subtotal carries round(…, 2) because a currency cell holds a whole number of minor units. A sum of many rows is exactly where that drifts.

Render it, and put the fragment where it belongs:

app.js
import { quario } from 'quario'
import { html } from '@quario/html'
import definition from './revenue.report.json' with { type: 'json' }

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

const fragment = await report.render(html(), data)   // your rows, from an API or DB

const page = `<!doctype html>
<html>
  <head><link rel="stylesheet" href="/report.css"></head>
  <body>${fragment}</body>
</html>`

What comes back is a fragment

That distinction is the whole shape of this target. The string is report markup only. A template can drop it into a layout, a mailer into an email body, a server straight into a response. Nothing in it fights the page around it, because nothing in it describes the page around it.

It also means the report isn’t styled yet. A fragment carries the classes and the text. It doesn’t occupy, break or paginate until a stylesheet says so.

Styling it

The q-* selectors are the contract — .q-group, .q-table, .q-item and friends — and they’re stable enough to write CSS against. The reference stylesheet carries the baseline look for anything a definition declared nothing about. A host that wants a different baseline moves it there rather than fighting inline declarations. Anything the definition did declare arrives as inline style and wins, which is what makes one definition read the same across targets. The full contract →

What this target doesn’t do

It doesn’t paginate, and it leaves page bands unread. page.header and page.footer draw nothing here, because a fragment has no pages to put them on. Page furniture belongs to your print CSS instead. A group’s break: "page" maps to a class rather than vanishing, so a print stylesheet has something to act on.

That makes a fragment plus a print stylesheet a real route to paper, and it’s the one the browser already knows how to drive. The trade is that pagination becomes the browser’s: repeated table headers and deterministic bytes aren’t part of that bargain. Where those matter, render the same definition through the PDF target instead.

FAQ

Is it safe to drop into a page?

Yes. This target escapes every interpolated value at the markup edge, and there’s no raw form to opt out of it. Your data can’t become markup, let alone script. quario renders without eval or code generation at all. This target treats literal text a definition author wrote as author-controlled, the same split every markup consumer makes. More on that →

How big is it?

The engine is under 5 kB minified and compressed, and the HTML target adds around 1 kB on top. There’s no stylesheet to ship unless you want the reference one, and no runtime beyond the render call.

Can I use it for free?

Evaluating it’s free and has no time limit — the full engine, no key to request — and evaluation output carries a watermark. Anything past evaluating it, including shipping it in an application or running it in your business, needs a seat per developer. Deployment is then unlimited and royalty-free, however many servers or users you have. See pricing →

A fragment, and the page you already have.

npm install quario
Getting started
© 2026 quario · KvK 61815977