---
title: "JavaScript report generator: HTML reports from data — quario"
description: "Generate a sectioned HTML report from data in JavaScript with one JSON report definition. quario returns a fragment you drop into your own page, with a documented class contract and no eval."
url: "https://getquario.com/guides/html-report-generator"
---

# 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:

```bash title="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:

```json title="revenue.report.json"
{
  "data": "$.sales[*]",
  "aggregates": { "total": "sum:=@.amount" },
  "header": [
    { "type": "text",
      "value": "Revenue by region",
      "style": { "size": 20, "bold": true, "spaceAfter": 10 } }
  ],
  "groups": [
    { "name": "region",
      "by": "=@.region",
      "aggregates": { "subtotal": "sum:=@.amount" },
      "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:

```ts title="app.ts"
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 →](https://getquario.com/docs/diving-deeper/html-target.md)

## 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` maps to classes rather than vanishing, so a print stylesheet has something to act on. `q-break` marks an instance whose leading edge turns, and `q-break-after` one whose trailing edge does.

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](https://getquario.com/guides/json-to-pdf.md) 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. The target escapes a definition’s own text the same way, so a report an agent wrote can’t reach the page as markup either. [More on that →](https://getquario.com/docs/diving-deeper/security.md)

### 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 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":"Guides","item":"https://getquario.com/guides"},{"@type":"ListItem","position":3,"name":"HTML reports","item":"https://getquario.com/guides/html-report-generator"}]}
```
