Styling
Intent
A definition styles itself in one closed vocabulary that belongs to no output format. Each target maps that vocabulary to its own model. That’s what lets a single document look like itself on a page, in a PDF, and in a workbook. The HTML target’s inline CSS is one such mapping, not the schema’s native tongue.
Design decisions
The vocabulary is a closed set, and the names aren’t CSS. They come from the banded-reporting tradition. An unknown name is a definition error rather than a declaration nothing reads, so a typo fails at compile time instead of rendering silently unstyled.
A declaration a target can’t honour is withdrawn, not invented. The XLSX
target doesn’t read uppercase, because a spreadsheet font carries no
text transform and writing capitals into the cell would turn presentation into
data. The cell would stop round-tripping and would sort differently. The CSV
target reads no styles at all.
Every value may be an expression. A literal that’s wrong is a definition error, while an expression result that’s wrong stays lenient at render and contributes nothing. That split runs through the whole vocabulary.
Style layers in five, outermost in. The target’s own baseline, then the
report default, then the band-role default a target supplies for headline
roles. Then the node’s own style, and last a styled run’s. A run is the
innermost layer, and everything that layers elsewhere layers there. A report
declaring a size therefore doesn’t resize a report header, whose band-role
default sits above the document’s.
The report default is a layer, not a merge. It’s narrowed to family and
size. Every other name is a definition error there.
A run carries the inline half of the vocabulary, and nothing else. family,
size, bold, italic, underline, strikethrough, color, background,
uppercase, format and currency. The box and flow spacing describe a block,
and align describes a line. Any of them on a run is a definition error naming
the run.
A family name is an intent the host resolves. sans, serif and mono
are the three every target resolves unaided. Any other name is a real typeface,
and a name isn’t a face. The HTML and PDF targets each take a font mapping
saying what it means on that surface. A generic name is the portable
declaration.
The box is border-box, per side, and doesn’t collapse. A border side is width, style and colour together, or none. Adjacent edges stay separate.
A table row’s style resolves onto that row’s cells. A row isn’t a
container that can hold a box on any surface. Not an HTML <tr>, not a
worksheet row, not a band of the page. So the engine resolves the row’s
declarations onto its cells before any target sees them.
API walkthrough
The full list of names is in the style declarations reference. This page covers the parts whose behaviour isn’t obvious from the name.
Setting a document default
style on the report states the typeface the document sets itself in. A
document with one face names it once instead of on every item, column, and cell.
{ "style": { "family": "sans", "size": 10 } }It’s resolved once per render, in report scope. It carries family and size
and nothing else.
Drawing a rule with the box
A definition declares padding and border per side, as
padding<Side> and border<Side>Width / border<Side>Style /
border<Side>Color.
{
"style": {
"bold": true,
"paddingTop": 5,
"borderTopWidth": 0.5,
"borderTopStyle": "solid",
"borderTopColor": "#1e1f22"
}
}An incomplete literal border is a definition error, so a side can’t be half
declared. A side that still holds an expression defers to render, where an
incomplete result contributes nothing rather than becoming a solid black
stroke. Width 0 with a style and a colour is complete and draws nothing, and
a named 0 beats whatever cell padding a target would otherwise supply.
A column’s width percentage is the border-box share, so padding and border
eat the wrapping width rather than adding to it.
Styling a whole row at once
A row’s style carries every declaration the vocabulary has, and the engine
resolves it onto that row’s cells. One declaration therefore draws the rule
under the column labels, or sets the pitch of the data rows, instead of one per
cell.
{
"detail": {
"header": {
"style": { "borderBottomWidth": 0.5, "borderBottomStyle": "solid", "borderBottomColor": "#1e1f22" }
},
"columns": [{ "header": { "value": "Item" }, "value": "{{ @.item }}" }]
}
}The consequences follow from the cells being the box. A row’s borderBottom is
one continuous edge under the row, because each covered cell draws its own. A
row’s borderLeft is an edge on every cell rather than one at the row’s outer
left. The row’s own left edge is a borderLeft on the first column’s cells.
This is where a row differs from a split, whose box stays its own.
Styling part of a line
A cell value may be a list of runs, each a { "value", "style" } pair.
That’s how you bold a word or format an amount inside a sentence. It survives
into the PDF and the workbook, where <b> in a template only ever meant
something to HTML.
{ "value": [{ "value": "Paid " }, { "value": "{{ @.amount }}", "style": { "bold": true, "format": "currency" } }] }A target may merge adjacent runs styled alike, and a single unstyled run is the same document as a plain string.
Aligning within a box
align is horizontal. valign is vertical, and it’s legal exactly
where a box has height it didn’t ask for. That means a table cell inside its
row and a split slot inside its split. On a stacked text item, a band image or
the report default it’s a definition error, because there is no slack to read.
The word is middle, not center. That one is already the horizontal name.
Naming a typeface
family takes sans, serif or mono, or a name the host maps.
import { html } from '@quario/html'
const target = html({ fonts: { Inter: 'var(--font-inter)' } })The PDF target maps the same name to TrueType bytes instead. Text that declares
no family renders in the report default’s face, and in the target’s own sans
where the report declares none.
The two mappings differ in reach. The HTML target’s resolves names a report
declared, and never the face of text that declared nothing. The PDF target’s
also replaces a generic’s base-14 faces, so mapping mono there reaches text
that named no typeface at all.
Showing and hiding
visible takes an expression, evaluated against the same scope the item’s
templates read. A hidden table cell keeps its column slot, so suppressing a
value leaves the column aligned.
A cell that’s empty or only whitespace contributes padding and no blank line. A hidden cell and an empty visible one stay the same height.