An embed is a read-only, live copy of one of your dashboards — or of a single widget — rendered inside someone else's page. It connects straight back to Hodoflow over a WebSocket, so it updates as new records land, exactly like the dashboard does in the app.
Everything about editing is absent by design. The embed is a viewer.
Outcome
A page outside Hodoflow shows one of your dashboards, or one widget, live, to visitors who are not signed in.
Before you start
- A saved dashboard with at least one widget.
- Somewhere you can edit HTML on the page that will host it.
- The decision that this dashboard's data can be seen by anyone holding a token. Anyone with the token can render it without signing in.
1. Get the snippet
Open the dashboard, press Share, and turn on Public embedding. An Embed snippet appears with Copy snippet beneath it. Paste it where you want the dashboard to appear:
<script src="https://app.example.com/assets/embed/v1/dash-embed.js"></script>
<dash-dashboard token="YOUR_SHARE_TOKEN" host="https://app.example.com" live="true"></dash-dashboard>
Both https://app.example.com values are your Hodoflow host, filled in for you
when you copy. The script registers the <dash-dashboard> element; the element
does the rest.
The script URL is versioned. v1 stays compatible for the life of the major
version, so the tag you paste today keeps working.
Checkpoint: the host page shows Connecting to dashboard… and then your widgets.
2. Verify the result
- The embed renders the same widgets as the dashboard.
- A new record produced by your workflow changes them without a page reload.
- Opening the host page in a private window, signed out, still works.
Attributes
| Attribute | Purpose |
|---|---|
token |
The dashboard's share token. Required. |
host |
Your Hodoflow origin, used for both the script and the socket. |
live |
"true" streams new data as it lands; "false" renders a static snapshot. Absent means live. |
tz |
The timezone to render in, as an IANA name like America/Chicago. Absent means UTC. |
preset |
A starting time range, using the same preset keys as the dashboard picker — last_7d, this_month, and so on. |
from / to |
A starting custom window, as ISO 8601 timestamps. Both are required together, and take precedence over preset. |
An older org attribute is still accepted for embeds that were written before
tokens became self-sufficient. New embeds should leave it out — omitting it is
what lets you rename your organization without breaking the integration.
Nothing outside that table is accepted, and no value in it is guessed at. An attribute Hodoflow does not recognise, or a value it cannot read, shows an error in place of the embed rather than rendering with a substitute — see What the embed shows.
Embed a single widget
<dash-widget> renders one widget on its own, outside any dashboard. Open the
widget in the widget library, press Share, and turn on Public embed:
<script src="https://app.example.com/assets/embed/v1/dash-embed.js"></script>
<dash-widget token="YOUR_WIDGET_SHARE_TOKEN" host="https://app.example.com"></dash-widget>
One script tag registers both elements, so a page using both only loads it once.
The widget's own share token is separate from any dashboard's, and the two are
never interchangeable: a dashboard token in a <dash-widget> renders Widget
not found or not publicly shared.
Everything above applies — the same attributes minus org, the same theming
properties, the same postMessage controls, the same part selectors. Two
differences:
- There is no dashboard header, and no grid. The element renders one card,
filling whatever height you give it; with no height set it falls back to
--dash-widget-h(320px). - The widget's own Filters are the only thing a runtime window is layered over, since there is no dashboard baseline above it.
A shared widget and a shared dashboard both occupy an embedded-asset slot on your plan.
Theming
The element renders inside a shadow root, so the host page's CSS cannot leak in and Hodoflow's cannot leak out. Theming happens through CSS custom properties you set on the element or any ancestor.
<style>
dash-dashboard {
--dash-accent: #10b981;
--dash-bg: #0b0f19;
--dash-card-bg: #131a2a;
--dash-text: #e5e7eb;
--dash-text-muted: #9ca3af;
--dash-border: #1f2937;
--dash-radius: 0.75rem;
--dash-font: "Inter", system-ui, sans-serif;
}
</style>
The full set:
| Property | Default | Controls |
|---|---|---|
--dash-accent |
#6366f1 |
Bar fills, line strokes, sparklines, markdown links, pivot totals |
--dash-accent-contrast |
#fff |
Text drawn on top of accent-filled surfaces |
--dash-font |
system sans-serif stack | Font family for the whole embed |
--dash-text |
#111827 |
Primary text |
--dash-text-muted |
#6b7280 |
Secondary text |
--dash-text-subtle |
#9ca3af |
Tertiary text |
--dash-bg |
#ffffff |
Root background |
--dash-bg-muted |
#f9fafb |
Muted surfaces |
--dash-border |
#e5e7eb |
Borders |
--dash-border-soft |
#f3f4f6 |
Internal dividers |
--dash-error |
#dc2626 |
Error text |
--dash-success |
#16a34a |
Positive values |
--dash-shadow |
0 1px 2px rgba(0,0,0,0.05) |
Default shadow |
--dash-card-bg |
inherits --dash-bg |
Widget card background |
--dash-card-border |
inherits --dash-border |
Widget card border |
--dash-card-shadow |
inherits --dash-shadow |
Widget card shadow |
--dash-radius |
0.5rem |
Corner radius |
--dash-grid-row-h |
70px |
Grid row height |
--dash-grid-gap |
0.75rem |
Gap between widgets |
--dash-padding |
1rem |
Outer padding |
--dash-widget-h |
320px |
Height a <dash-widget> falls back to |
--dash-accent is re-read shortly after mount as well as during it, so setting
it from JavaScript after the element has appeared still takes effect. Pie charts
deliberately ignore it and keep their multi-colour palette, since one accent
cannot distinguish segments.
For anything the properties do not reach, every meaningful node carries a part
attribute, so ::part() selectors work from the host page: root, header,
title, grid, widget, widget-header, widget-body, table, feed,
pivot, markdown, and the state nodes.
Controlling the embed at runtime
The embed listens for two postMessage messages on window, which is how a
host page's own controls can drive it without re-mounting the element.
Change the time range:
window.postMessage({ type: 'dash:time_range', preset: 'last_7d' }, '*');
window.postMessage({
type: 'dash:time_range',
from: '2026-07-01T00:00:00Z',
to: '2026-07-31T23:59:59Z'
}, '*');
from and to together win over preset. A preset of all_time or custom
clears the range. If Hodoflow rejects the range, the element dispatches a
dash:filter_rejected event you can listen for on the element itself.
Pause and resume live updates:
window.postMessage({ type: 'dash:live', live: false }, '*');
Any live value other than false resumes. A runtime toggle overrides the
live attribute.
What the embed shows
While connecting: Connecting to dashboard… (or Connecting to widget…).
If the token is wrong, or the dashboard is no longer shared: Dashboard not found or not publicly shared. — which is also what you will see right after using Regenerate token without updating the snippet.
If the dashboard has no widgets yet: No widgets on this dashboard yet.
If an attribute is wrong, the error appears in place of the embed and it names
what is wrong — Invalid tz attribute: "America/Chigago".,
Invalid live attribute: "yes". Use "true" or "false".,
Unknown attribute: "timezone". Nothing renders, deliberately: a bad zone or a
window Hodoflow could not read would otherwise show numbers that are quietly
wrong, and an embed has no viewer who could tell.
Any other failure renders as Connection error: followed by the reason.
Common problems
Dashboard not found or not publicly shared. The token is wrong, the dashboard is no longer shared, or the token was regenerated without updating the snippet.
Widget not found or not publicly shared. The same, for <dash-widget> — or
a dashboard token pasted into a <dash-widget> (or the reverse). The two token
spaces are separate.
Connection error: followed by a reason. The host attribute is wrong, or
the host page is blocking the WebSocket.
Nothing but Connecting to dashboard…. The script loaded but the socket did
not open. Check host matches your Hodoflow origin exactly, including the
scheme.
No widgets on this dashboard yet. The dashboard is empty. Add widgets and save.
Today shows different numbers than the app. With no tz attribute, calendar
presets in an embed resolve in UTC, so Today means the UTC day. Set
tz="America/Chicago" (or whichever zone the page's readers are in) to match
what you see inside Hodoflow. Rolling windows like last_7d are the same in
every zone and are unaffected.
An invalid attribute error you did not expect. Only the documented
attributes are accepted. Framework-added attributes (class, style, data-*,
aria-*, and directives like v-/x-/ng-) are left alone; a plain
unrecognised name is treated as a typo, because from outside there is no way to
tell one from the other.
A token leaked. Use Regenerate token — every existing embed stops working immediately, which is the point. Turning Public embedding off revokes the whole thing.
Next step
Build a dashboard for the sharing controls and the widget reference.