Examples
Basic Usage
Responsive D3 chart from a CDN
The evidence API
Every script inside the block can reach a singleevidence object. It is the only bridge to the page — data, variables, theme, filters, and lifecycle all hang off it.
The reliable pattern is to wrap your draw in
async function render() { … }, then evidence.subscribe(render); await render(); — evidence.query returns fresh rows each call, so the same function serves the first render and every reactive re-render.
Passing Variables
The{% html %} block runs verbatim in an isolated iframe: the body is never interpolated, so the way a page-level value (frontmatter, a component attribute, a repeat-scoped value, a filter value, a literal) reaches your code is the variables={…} attribute. Nothing crosses into the sandbox that you didn’t pass. Values are evaluated on the page, snapshotted into the iframe, and read as evidence.variables.
{{ }}, so each iteration gets a different evidence.variables.
{{ $x }}does not interpolate inside the block — the body is verbatim. For plain text, write it in markdown outside the block (where{{ $x }}works normally); inside the block, pass the value viavariables=and readevidence.variables.xfrom a script. Validation catches both mistakes with the exact fix.- Reading a value you didn’t pass returns
undefined— onlyvariables=entries exist inside the sandbox. Validation flags visible reads with no matching entry. - Reactivity:
const speed = evidence.variables.speedat the top of your script captures the value once — when the attribute or filter behind it changes, your constant does not.evidence.variablesitself is always current, so either read it where you use it (e.g. inside your render/animation loop), or registerevidence.onVariablesChange((vars) => { /* re-render */ })for structural changes. A change that arrives while nothing is listening logs a console warning explaining this. - Values must be serializable primitives (string / number / boolean / null). Objects, arrays, and functions are dropped before the snapshot reaches the iframe — flatten them at the call site (
start=$period.start), or query them throughevidence.query()instead. evidence.variablesis a snapshot; mutating the returned object doesn’t change anything (each read returns a fresh shallow copy).- For row data, prefer
evidence.query("query_name")over packing rows intovariables=— query results stream lazily and aren’t limited to primitives.
Parameterizing Queries from JS
evidence.query() takes no parameters. To re-run a query with different inputs, create a filter in your JS and reference it from the SQL: the query re-runs server-side, and the predicate is applied on the warehouse, so only matching rows enter the iframe. Prefer this over pulling a whole table and filtering client-side.
The loop has four steps:
- Declare the filter in your script:
evidence.filters.create("region", "north")(create it before anything.sets it). - Reference it from any sql fence with
{{ region }}(quoted value) or{{ region.literal }}(raw, for numbers). - Set it from your interaction handler:
evidence.filters.set("region", picked)— the query re-runs on the warehouse. - React:
evidence.subscribe(render)fires when the fresh result lands; callevidence.query()again insiderenderto get the new rows.
evidence.query() and filter/redraw client-side — a warehouse round-trip per frame is the wrong tool. Use the filter loop when the table is too big to pull, or when other components on the page should react too (evidence.filters.create(id, value, { column: "the_column" }) makes built-in charts with filters="id" follow your selection).
Sizing and Responsiveness
By default the block autosizes: it grows and shrinks to fit its content height and fills the page width. Give your content a real height so it has something to size to:- A fixed-pixel element, or a responsive SVG sized with
viewBox+width:100%+height:auto(which takes its height from the aspect ratio). - A
height:100%element has nothing to fill in autosize mode. For a chart that fills a fixed box (canvas, ECharts, and Chart.js all read the container’s size), passheight=in pixels on the tag — that pins the box and makes the mount area full-height, soheight:100%works.
viewBox + width:100% (no redraw needed), or redraw from evidence.onResize(cb) (call the library’s resize method in the callback for canvas/ECharts/Chart.js). A hardcoded pixel width won’t reflow.
The block is an isolated iframe, so its own width is the viewport width — CSS @media (max-width: 480px) queries fire at the block’s width, which makes them behave like container queries. Use them to reflow at narrow widths (stack columns, shrink type).
Tooltips and popovers are clipped at the block edges — CSS overflow can’t escape the frame. Position them relative to your own container and clamp into bounds (e.g. left = Math.max(0, Math.min(x, mount.clientWidth - tip.offsetWidth))), or flip them near an edge.
Network Allowlist
Author code inside an{% html %} block runs in a sandboxed iframe with a content-security-policy that blocks all network traffic except to the curated hosts below. fetch, XHR, d3.csv, and d3.json work against these hosts; everything else is blocked at the browser level.
For data from the user’s own report, always use evidence.query("query_name") instead — page rows live in the parent context and have no URL to fetch.
Script CDNs
Used for loading JS libraries via<script src> or import:
https://cdn.jsdelivr.nethttps://esm.shhttps://esm.runhttps://unpkg.comhttps://cdnjs.cloudflare.comhttps://d3js.org
Map tiles
Available to both<img> tag-based map libraries (Leaflet raster) and modern fetch/WebGL libraries (deck.gl, MapLibre):
https://tile.openstreetmap.orghttps://a.tile.openstreetmap.orghttps://b.tile.openstreetmap.orghttps://c.tile.openstreetmap.orghttps://a.basemaps.cartocdn.comhttps://b.basemaps.cartocdn.comhttps://c.basemaps.cartocdn.comhttps://d.basemaps.cartocdn.comhttps://tiles.stadiamaps.comhttps://server.arcgisonline.comhttps://services.arcgisonline.comhttps://maps.wikimedia.org
Images
Image-only hosts (loadable in<img> tags but not via fetch):
https://upload.wikimedia.orghttps://commons.wikimedia.orghttps://flagcdn.com
Data and public APIs
Reachable fromfetch, XHR, d3.csv, d3.json. Includes GeoJSON / TopoJSON / Atlas files on the data CDNs (e.g. unpkg.com/world-atlas@2/countries-110m.json) plus keyless public-data APIs:
https://cdn.jsdelivr.nethttps://unpkg.comhttps://raw.githubusercontent.comhttps://api.frankfurter.apphttps://restcountries.comhttps://api.worldbank.orghttps://api.open-meteo.comhttps://www150.statcan.gc.ca
Need data from another host?
For data from your own warehouse, useevidence.query("query_name") — page queries aren’t subject to this allowlist. To reach an external host that isn’t listed, ask your Evidence admin to add a project-level allowlist entry for it.
Attributes
number
Set the width of this component (in percent) relative to the page width
number
Set a fixed height for the chart in pixels
object
Frontmatter (
$var), filter or repeat values ("{{ my_filter.literal }}"), and literals to expose to the iframe as evidence.variables. Write variables={ name=$frontmatter_name region="{{ region.literal }}" limit=10 } (Markdoc object syntax: whitespace-separated key=value, no commas). Filter/repeat values must be quoted {{ }} and use a real property — .literal (raw) or .selected (quoted for SQL); there is no .value. Changing a value triggers evidence.onVariablesChange(cb) / evidence.subscribe(cb) inside the iframe.
