The <halo-report> element

Every attribute, method and event of the web component that embeds a Halo report, with the filter values it takes and the errors it reports.

<halo-report> is a web component that draws a report in an iframe and lets your page talk to it. It adds about 2 KB gzipped, with no React and no other dependencies. For where to get an embed link and how to list your site, start with Embed a report on your site.

<script type="module" src="https://cdn.usehalo.net/halo-report.js"></script>
<halo-report src="https://{org}.usehalo.net/embed/hle_…"></halo-report>
<script type="module" src="https://cdn.usehalo.net/halo-report.js"></script>
<halo-report token="SIGNED_EMBED_TOKEN" origin="https://{org}.usehalo.net"></halo-report>

Load the script once per page.

Attributes

AttributeValueDefault
srcAn embed link, https://{org}.usehalo.net/embed/hle_…
tokenA signed embed token. Used when there's no src
originYour organization's Halo address, where a token opensThe script's own origin
elementOne element's id, drawn alone to fill the frame. A signed token's element claim wins over itThe whole report
editionOne edition's slug, such as 2026-03, on a report with editions. For a signed token, its edition claim decides insteadThe latest
themelight, dark or systemlight
filtersJSON: reader filters to start with, by field. See Filter valuesThe report's own
stateWhat a filtered link carries, as its query string: page=markets&region=North. It's what halo:filterchange reports, so you can keep it and open the same view laterThe report as published
titleThe iframe's accessible name. Set it before the element connects"Report", then the report's title

With neither src nor token, the element shows a note saying so.

What happens when an attribute changes

AttributeEffect
src, token, origin, element, editionThe frame reloads
stateThe frame reloads, unless it's the state the frame last reported
themeThe mode changes without a reload
filtersThe new value is applied as with setFilters(), once the frame is ready
titleNothing until the frame next draws

The frame loads lazily, so halo:ready waits until the element is near the viewport.

The address it opens

The element builds the frame's address from its attributes, and an iframe can use the same query parameters:

{src}?element={element}&edition={edition}&{state}&theme={theme}
{origin}/embed/signed?element={element}&{state}&theme={theme}#token={token}

Methods

MethodWhat it does
setFilters(filters)Sets reader filters by field, as a reader's pick. Before the frame is ready, they wait and go with its first message. Fields you don't name stay as they are
getFilters()The filters set now, by field, as the frame last reported them, a copy you can keep. Before the frame is ready, what will be sent
reset()Returns every filter to how the report's author left it

The report property holds the report's id once the frame is ready.

const report = document.querySelector("halo-report");

report.addEventListener("halo:ready", () => {
  report.setFilters({ region: ["North", "South"] });
});

report.addEventListener("halo:click", (event) => {
  console.log(event.detail.element, event.detail.values);
});

report.addEventListener("halo:error", (event) => {
  console.warn(event.detail.code, event.detail.message);
});

Events

All are CustomEvents that bubble and cross the shadow boundary, with their values in detail.

EventdetailWhen
halo:ready{ report, title, filters, edition, state }The report has drawn and can take filters. edition is the slug of the edition showing, on a report with editions. state is the reader's view as a link's query string
halo:filterchange{ filters, edition, state }The filters or the page changed, by a reader or by you
halo:click{ element, values }A reader clicked a mark on a chart that takes clicks. values has the clicked values, by field, in the shape setFilters() takes
halo:error{ code, message }Something went wrong. See Errors
halo:resize{ height }The content's height changed, in pixels

To have a click on one embed filter another, pass it across:

first.addEventListener("halo:click", (event) => second.setFilters(event.detail.values));

Filter values

Filters are an object keyed by field name. Each lands on the report's controls over that field, as if a reader had picked it, so the report's own rules for scope, targets and dependent choices apply. null clears a field.

ControlValueExample
Single selectOne value{ "region": "North" }
Multi-select with searchOne value, or a list{ "region": ["North", "South"] }
Text searchText or a number. Blank clears it{ "product": "desk" }
Number range[low, high], either null for open{ "amount": [100, null] }
Toggletrue turns it on, and false off{ "is_member": true }
Date range with presets, Relative dates{ start, end } or [start, end], as YYYY-MM-DD{ "order_date": ["2026-05-01", "2026-10-31"] }
Date range with presets, Relative datesA relative range{ "order_date": { "kind": "relative", "preset": "last_n_days", "n": 30 } }
Top NCan't be set from outside

Relative presets are today, yesterday, last_n_days, last_n_weeks, last_n_months, this_week, last_week, month_to_date, quarter_to_date, year_to_date, last_month, last_quarter, last_year, rolling_12_months, fiscal_quarter_to_date, fiscal_year_to_date, last_fiscal_quarter and last_fiscal_year. The last_n_* presets need n.

You can only set fields the report has a control for. To filter by something else, add a filter control to the report, or use a signed embed's locked filters.

Errors

halo:error carries a code and a message in plain words.

CodeMeans
filter_not_availableThe report has no control for that field, so it was left out
filter_lockedA signed token fixes that field, so it stays as it is
invalid_filterThe value doesn't fit the control, such as text for a date range
link_not_foundThe link was turned off or its report removed
link_expiredThe link has expired
edition_not_foundThe report has no edition by that name
edition_withdrawnThat edition was withdrawn. The message ends with the note its author left
locked_filter_goneA filter fixed on the link no longer fits the report's data, so it isn't shown. Publishing the report again fixes it
report_not_publishedThe report has never been published
invalid_tokenA signed token that didn't verify, or expired. Mint a new one
element_not_foundThe element isn't in the published report
embed_domain_not_allowedA signed embed on a site the report doesn't allow
forbiddenThe report can't be opened from here
networkThe frame couldn't reach Halo
rate_limitedToo many requests. The report is busy

Any other code is the public API's, passed on. A filter_* error doesn't stop the report, and the other filters in the same call still apply. A chart that asks for a field the report doesn't share says so in that chart, and your page hears nothing.

Size

The frame takes its content's height, and updates as it changes, unless your page sizes the element. Give the element a height or min-height, and the frame fills that instead:

halo-report { height: 720px; }

A one-element embed asks for that element's own height. The element is display: flex and takes the hidden attribute.

Editions

On a report with editions, an embed opens the latest, with no menu to change it, unless you pin one with edition. An embed link shows the published figures. Its token decides for a signed embed. A signed embed asks your data again as of that edition's date, under your locked filters and row policies.

Themes

Embeds start light, since the page around them decides. system follows the reader's setting and updates when it changes. A report that fixes its own mode keeps it. See Your brand on reports.

How it talks to the frame

The element and the frame exchange postMessage objects marked protocol: "halo-report" and version: 1, and you don't need to send them yourself.

  • The element accepts messages only from its own iframe's window, at that iframe's origin, and posts only to that origin.

  • The frame ignores everything until a hello from its parent window, fixes the parent's origin from it, then accepts only that window and origin.

  • A plain iframe never says hello, so the frame never posts to it. That's why an iframe embed has no events, filter API or automatic height.

DirectionTypes
Element to framehello, setFilters, reset, theme
Frame to elementready, filterchange, click, error, resize

To keep a reader's place, store event.detail.state from halo:filterchange and set it as state next time. An iframe embed takes the same query string in its address. Unlike a public link, an embed never changes the address of the page around it.