---
title: Runtime attributes
description: The data-dash roles and every data attribute the Dashies dashboard runtime reads, including the viewer controls and time intelligence.
updated: 2026-08-05
---

A published dashboard is one HTML file. The runtime finds every element carrying
`data-dash`, reads the other `data-*` attributes on it to learn what to render,
and resolves each value against the data island.

You get these attributes for free when you publish
[a spec with `tiles`](/reference/spec). You write them by hand only in
[look mode](/reference/spec#look), where the markup is yours. Either way the same
runtime reads them, and every binding must resolve against a declared dataset or
the publish is refused.

## Roles

`data-dash` names what an element is. Seventeen values are recognised.

| Role | Rendered by the spec tile |
|---|---|
| `metric` | `kpi` |
| `chart` | `chart` |
| `table` | `table` |
| `matrix` | `matrix` |
| `heatmap` | `heatmap` |
| `scatter` | `scatter` |
| `treemap` | `treemap` |
| `waterfall` | `waterfall` |
| `funnel` | `funnel` |
| `drilldown` | `drilldown` |
| `stacked` | `stacked` |
| `combo` | `combo` |
| `pie` | `pie` |
| `donut` | `donut` |
| `gauge` | `gauge` |
| `filter` | `filter` |
| `updated-at` | emitted once per dashboard, not by a tile |

The `kpi` tile compiles to `data-dash="metric"`; that is the only case where the
tile type and the role differ in name. The `text` and `custom` tiles carry no
`data-dash` at all: one is prose and the other is markup you own.

`data-dash="updated-at"` marks the span that receives the freshness stamp. It
takes no other attributes.

## Routing

| Attribute | Value | Notes |
|---|---|---|
| `data-dataset` | a dataset name | Which dataset this tile reads. Emitted on every bound tile when the dashboard declares more than one; with a single dataset it is omitted and the runtime uses that one. |

## Binding a value

| Attribute | Value | Read by |
|---|---|---|
| `data-measure` | a measure key | Every role that shows a figure. |
| `data-measures` | comma-separated measure keys | `chart`, for a multi-series chart. |
| `data-measure2` | a measure key | `combo`, the secondary axis. |
| `data-num`, `data-den` | measure keys | Any role showing a ratio. The ratio is computed under the current filters, never stored. |
| `data-num-scope`, `data-den-scope` | `all` | Computes that side of the ratio over the unfiltered dataset, which is how a share-of-total is expressed. |
| `data-num2`, `data-den2` | measure keys | `combo`, a ratio on the secondary axis. |
| `data-agg` | an aggregate name | An override the compiler never emits. It is honoured only where it agrees with the stored measure; a claim the data cannot support renders unavailable. |

## Naming a dimension

| Attribute | Value | Read by |
|---|---|---|
| `data-x` | a dimension key | `chart`, `stacked`, `combo`, `treemap`, `waterfall`, `funnel`, `pie`, `donut`. |
| `data-rows`, `data-cols` | dimension keys | `matrix`, `heatmap`. |
| `data-point` | a dimension key | `scatter`, one point per member. |
| `data-levels` | comma-separated dimension keys | `drilldown`, outermost first. |
| `data-series` | a dimension key | `chart`, `stacked`. |
| `data-dim` | a dimension key | `filter`. |

## Shape and scale

| Attribute | Value | Read by |
|---|---|---|
| `data-type` | `bar`, `hbar`, `line`, `area` | `chart`, `stacked`. |
| `data-type2` | `bar`, `line`, `area` | `combo`, the secondary series. |
| `data-stack` | `normal`, `percent` | `stacked`. |
| `data-axis-sync` | present | `combo`, forces both axes onto one scale. |
| `data-subtotals` | `both`, `row`, `col`, `none` | `matrix`, `heatmap`. |
| `data-color` | `heat`, `diverging` | `matrix`, `heatmap`. |
| `data-min`, `data-max`, `data-target` | numbers | `gauge`. These are numeric literals, not measure keys. |
| `data-stages` | comma-separated dimension values | `funnel`, in the order the funnel runs. |
| `data-top-n`, `data-other`, `data-total` | a number, then two flags | `drilldown`. |

## How much, and how big

| Attribute | Value | Read by |
|---|---|---|
| `data-limit` | an integer | Members rendered before truncation. The ceiling differs per role: see [Tile types](/reference/tiles#where-a-tile-truncates). |
| `data-sort` | `value-desc`, `value-asc`, or `<key>:asc` / `<key>:desc` on a table | `chart`, `table`. |
| `data-columns`, `data-group` | column keys | `table`. |
| `data-height` | 120 to 800 | Every drawn chart role. |

## Display

| Attribute | Value | Notes |
|---|---|---|
| `data-label` | a string | The visible label, on a `filter`. |
| `data-format` | a format name | How to render the figure. `data-format2` is the `combo` secondary. |
| `data-currency` | an ISO 4217 code | Emitted only where it differs from the dashboard's global currency. |
| `data-decimals` | 0 to 6 | Same rule. |
| `data-scale` | a divisor | **Inert.** The compiler emits it on a ratio binding and no runtime fragment reads it, so it changes nothing. It is why a measure declaring `scale: cents` or `scale: points` is [refused at publish](/reference/spec#units) rather than rendered at 100 times its value. |

## Viewer controls

These are what make a published page interactive without a server. Each is read
at view time and changes only what is on screen.

| Attribute | Value | Effect |
|---|---|---|
| `data-multi` | present | The `filter` becomes a multi-select listbox instead of a single-select. |
| `data-range` | present | The `filter` becomes a range selector. A tile may carry `data-multi` or `data-range`, never both. |
| `data-controls` | control names | Adds live sort and limit controls to a `chart` or `table`, so a reader can reorder or widen it. |
| `data-xfilter` | present | Clicking a bar filters the rest of the dashboard by that member. |
| `data-drill` | a dataset name | Clicking the tile opens a modal showing that dataset's detail for the clicked member. |
| `data-tile` | a tile id | The identity a control or cross-filter addresses. Emitted alongside the two above. |

A multi-select or range filter changes which cells the resolver has to combine,
which is why declaring one can change the dataset mode a spec needs. See
[how `mode` is resolved](/reference/spec#how-mode-is-resolved-when-you-omit-it).

## Time intelligence

Four attributes, on a `metric` tile only. **The compiler does not emit these**;
they are available in look mode and on hand-written markup.

| Attribute | Value |
|---|---|
| `data-timeintel` | the date dimension key to walk |
| `data-compare` | `yoy`, `qoq`, `mom`, `pop`, `ytd`, `running`, `movavg` |
| `data-compare-as` | `pct`, `delta`, `prior`, `current`. Defaults to `pct`. Applies to a shift mode only |
| `data-window` | 2 to 1,000, required by `movavg` |

`yoy`, `qoq` and `mom` shift the anchor period back 12, 3 and 1 months. `pop`
steps to the previous period actually present in the series rather than a
calendar guess. `ytd` and `running` accumulate up to and including the anchor.

Every one of these resolves at view time out of cells a lattice already
precomputed, so the comparison stays correct under whatever filter is applied.
**A comparison composes two resolutions, so both must be exact**: where a prior
period is missing, the tile renders unavailable rather than treating the absence
as a zero and reporting a confident growth figure.

That means a comparison needs a `lattice` or `hybrid` dataset. On a plain `cube`
or a row-level dataset the tile says so and renders nothing.

## What a tile cannot do

A published dashboard is served with `Content-Security-Policy: sandbox
allow-scripts`. Scripts run, including a `custom` tile's `js`, but the page is
placed in an **opaque origin**. That is not configurable.

| Capability | Under the sandbox |
|---|---|
| Script execution | allowed, which is what `allow-scripts` grants |
| `localStorage`, `sessionStorage`, IndexedDB, cookies | **denied**, and access throws |
| Same-origin authenticated fetch | **denied**, because the origin is opaque |
| Cross-origin `fetch` | **not restricted** |

:::warning{title="This isolates the page from your Dashies account. It does not confine the page."}
There is no `connect-src` and no `default-src` in that header, so a
cross-origin `fetch` from a `custom` tile is **not** blocked. The product relies
on exactly that: the row-level query engine is loaded cross-origin from
`data.dashies.xyz` under this same header, and that cross-origin split is what
preserves the isolation in the first place.

What the sandbox buys is the thing that matters on a shared origin. A published
dashboard is served from a host the Dashies app also uses, so without the opaque
origin any markup an author included could read a viewer's session out of
`localStorage`. The sandbox makes that impossible. It is not a promise that the
page talks to nothing.
:::

Two variants add a framing rule on top: a private dashboard is served with
`frame-ancestors 'none'`, and a preview with `frame-ancestors 'self'`. Those
constrain who may frame the page, not what the page may read.

The practical consequence for a `custom` tile is still that it should draw only
from the datasets named in its `reads`, which the runtime hands it directly.
Anything it fetches itself is outside the refresh contract: it is not versioned,
not seeded, not re-run on the schedule, and not covered by any of the
correctness checks on this site.

## Check it worked

Open the published dashboard and change a filter. Every bound tile should either
update or state a reason it cannot. A tile that renders a stale figure after a
filter change is reading something other than the resolver, which for a `custom`
tile means its `js` is holding its own copy of the numbers.
