Duck-UIDuck-UI

Dashboards

Markdown documents with live SQL, charts, and input variables, rendered entirely in your browser

Dashboards in Duck-UI are markdown documents with live SQL inside. You write text, declare named queries in code fences, and place components that bind to those queries by name. The document renders as a report, the queries run in your browser against your data, and nothing ever touches a server.

If you have used Evidence, the syntax will feel familiar on purpose. If you have used Grafana, the input variables will.

Creating a dashboard

Three ways in:

  1. Dashboards panel (sidebar): type a name, click New. The dashboard opens with a starter document that already works.
  2. Add to dashboard from any query result: the query and a table component are appended to the dashboard you pick (or a new one).
  3. Open a shared link someone sent you (see Sharing below).

Dashboards are saved automatically to your browser's storage, per profile. They survive reloads and browser restarts, and they show up in the Dashboards panel as a list.

Anatomy of a document

---
title: Sales overview
---

# Q3 sales

Revenue keeps climbing:

```sql revenue
SELECT region, sum(amount) AS total
FROM sales
GROUP BY region
ORDER BY total DESC
```

<BarChart data={revenue} x=region y=total/>

The winner is {revenue[0].region} with {revenue[0].total}.

Three building blocks:

  • Named SQL fences — a fenced code block whose info line is sql plus a name (```sql revenue). The name must be a valid identifier. Every fence runs against the dashboard's connection, and components reference the result by that name.
  • Components — self-closing tags like <BarChart data={revenue} x=region y=total/>. Unknown tags render their raw source instead of breaking the page, so a typo is visible, not fatal.
  • Interpolation{query_name[0].column} inside text inserts a single value from a result. It is a lookup, not an expression language: no code runs from a document.

A component tag written inside backticks or a code fence is documentation, not a live component. That is how this page shows them without rendering them.

Editing

Open a dashboard and click Edit for a split view: markdown source on the left, the live document on the right. The preview re-renders as you type, and only the queries whose SQL actually changed re-run.

The editor knows the dialect:

  • Type < to get a component list. Accepting one inserts a working scaffold with tab-through placeholders (data={…}, x=, y=), not just the tag name.
  • Inside data={ you get the names of the queries declared in this document.
  • Inside an open tag you get that component's props.
  • Type ${ inside a SQL fence to get the input variables declared in the document (inputs.region.value, inputs.period.start, and so on).
  • Snippets scaffold a named sql fence and the frontmatter block.

Ctrl+Space (or Cmd+Space) opens suggestions anywhere.

Click Done to go back to the full-width report. Refresh all re-runs every query.

Components

Charts

All charts bind the same way: data={query_name} plus column props.

TagRendersNotes
<LineChart/>linex, y, optional series for one line per category
<TimeSeries/>linealias of LineChart
<Sparkline/>linecompact, no axes
<BarChart/>barsx, y, optional series
<Histogram/>barsalias of BarChart
<AreaChart/>areafilled line
<ScatterPlot/>scattertwo numeric axes
<BubbleChart/>bubblescatter plus size
<PieChart/>piex labels, y values
<DonutChart/>donutpie with a hole
<FunnelChart/>funnelstage drop-off
<BoxPlot/>boxdistribution per category
<Heatmap/>heatmapmatrix intensity

Common props: x, y, series (split into one series per distinct value), title.

```sql by_region
SELECT region, month, sum(amount) AS total FROM sales GROUP BY 1, 2
```

<LineChart data={by_region} x=month y=total series=region title='Monthly by region'/>

Tables and values

TagWhat it does
<DataTable data={q}/>full result table with sorting and filtering
<BigValue data={q} column=total agg=sum title='Revenue'/>one headline number
<Value data={q} value=total/>a value inline in a sentence
<Delta data={q} column=growth/>change indicator, colored up/down
<DownloadData data={q} title='Download CSV'/>CSV download button

Aggregations for agg: sum, avg, min, max, count, first.

Layout

TagWhat it does
<Grid cols=2>…</Grid>side-by-side layout for whatever is inside
<Details title='Methodology'>…</Details>collapsible section
<Alert status=warning>…</Alert>callout box (info, warning, error, success)
<LinkButton url='https://…' title='Open'/>link styled as a button

Input variables

Inputs turn a static report into an interactive one, Grafana-style. Declare an input component in the document, then read its value in SQL with ${inputs.name.value}:

<Dropdown name=region options='north,south,east' title='Region'/>

```sql filtered
SELECT * FROM sales WHERE region = ${inputs.region.value}
```

<DataTable data={filtered}/>

Changing the dropdown re-runs only the queries that reference it. Values are substituted as escaped SQL literals before execution, so a document's inputs cannot inject SQL.

TagValueReads as
<Dropdown name=x options='a,b,c'/>one of the options${inputs.x.value}
<ButtonGroup name=x options='a,b,c'/>one of the options${inputs.x.value}
<TextInput name=x/>free text${inputs.x.value}
<DateInput name=x/> / <DatePicker name=x/>a date${inputs.x.value}
<DateRange name=x/>two dates${inputs.x.start} and ${inputs.x.end}
<Slider name=x min=0 max=100 step=1/>a number${inputs.x.value}
<Checkbox name=x/>true/false${inputs.x.value}

Dropdowns can also be query-backed: <Dropdown name=region data={regions} value=region_name/> builds its options from a query result.

Every input has a default, so every query can run before anyone touches anything.

Execution and caching

Queries run on the dashboard's connection (your in-browser DuckDB by default). Results are cached by the final SQL text, so:

  • two components bound to the same query share one execution,
  • editing one query re-runs only that query,
  • changing an input re-runs only the queries that reference that input.

Query results stay in your browser. They are never part of the document, never synced, never in a share link.

Sharing

The Share button on a dashboard offers two links, and a live session offers a third mode:

ModeWhat the recipient getsEnforcement
Viewer linkthe document opens read-onlya workflow signal, not a lock — the source is in their browser, and they can save an editable copy deliberately
Editor linkthe document imports as an editable copy in their profiletheir copy, their storage
Live sessionreal-time co-editing with cursors, host computeenforceable: the host can revoke at any time

Share links carry the source only — markdown and SQL, never results, never credentials. The payload travels in the URL fragment (#dash=…), which browsers do not send to servers, so your SQL stays out of access logs.

One honest caveat: a shared document's queries reproduce for the recipient only if the data they read is reachable from the recipient's browser (remote parquet/CSV yes, your locally imported tables no). Pair a share with a live session or a fork when the data has to travel too.

Co-editing in live sessions

Any dashboard open during a live session becomes co-editable: everyone in the session edits the same source with character-level merging and visible cursors, and participants who just view it see the report re-render live as others type.

On this page