# Embeds

> Drop a live Hodoflow dashboard into any web page with one script tag and one custom element, themed with CSS custom properties.

An embed is a read-only, live copy of one of your dashboards, 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.

## 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:

```html
<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.

## 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. |
| `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. Takes 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.

## 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.

```html
<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-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:

```javascript
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:

```javascript
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…**

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.**

Any other failure renders as **Connection error:** followed by the reason.

## Things to know before you ship one

Calendar presets in an embed resolve in UTC rather than a viewer's timezone, so
**Today** in an embed means the UTC day. It can differ from the same dashboard
viewed inside Hodoflow, where it follows your profile timezone.

Anyone with the token can render the dashboard without signing in. Treat the
snippet as a credential, and use **Regenerate token** if it leaks — every
existing embed stops working immediately, which is the point.

Turning **Public embedding** off revokes the whole thing. See
[Dashboards](/help/data-dashboards/dashboards) for the sharing controls.
