Authoring HTML
Experimental. This feature may change or be removed.
Your shell is a normal HTML document loaded in an Admin iframe. The host talks to it with window.postMessage.
There is no custom-widget library and no requirement to host the page yourself.
Contract
1. Shell signals ready
When your page has registered message listeners and is ready for data:
parent.postMessage({ type: "dashboard:ready" }, "*");
Post ready after you attach the message listener so the first host reply is not dropped.
2. Host sends data, theme, and optional style
The host listens for dashboard:ready from your iframe, loads bound data, then posts:
| Message type | Payload | Purpose |
|---|---|---|
dashboard:data | { source, data } | Bound source descriptor and resolved payload. source may be null. |
dashboard:theme | { theme } | "dark" or "light" to match Admin. |
dashboard:style | { style } | Optional host-driven style object. May be empty. |
dashboard:data is the custom shape: { source, data }.
Theme may be sent again when the Admin color scheme changes, without a full shell reload.
3. Header refresh
If the user clicks data refresh in the header, the host re-fetches and posts dashboard:data and theme again.
Keep handlers idempotent so a second payload redraws cleanly.
Minimal example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Custom Dashboard</title>
<style>
:root {
color-scheme: light dark;
}
body {
font-family: system-ui, sans-serif;
margin: 1.5rem;
}
pre {
overflow: auto;
padding: 1rem;
border: 1px solid #ccc;
}
</style>
</head>
<body>
<h1>Custom Dashboard</h1>
<p id="theme">theme: ...</p>
<pre id="out">waiting for data...</pre>
<script>
const out = document.getElementById("out");
const themeEl = document.getElementById("theme");
window.addEventListener("message", (event) => {
// Prefer checking event.origin against the Admin origin in production
// (the parent that posts data/theme/style), not the API host of this HTML.
const msg = event.data;
if (!msg || typeof msg !== "object") return;
if (msg.type === "dashboard:data") {
// Custom shape: { source, data }
out.textContent = JSON.stringify({ source: msg.source, data: msg.data }, null, 2);
}
if (msg.type === "dashboard:theme") {
document.documentElement.dataset.theme = msg.theme; // "dark" | "light"
themeEl.textContent = "theme: " + msg.theme;
}
if (msg.type === "dashboard:style") {
Object.assign(document.body.style, msg.style ?? {});
}
});
// Signal ready so the host can inject data + theme
parent.postMessage({ type: "dashboard:ready" }, "*");
</script>
</body>
</html>
This matches the snippet shown on the empty state in Admin.
Origin checks
There are two directions. Do not mix them up:
| Who listens | What event.origin is | What to allowlist |
|---|---|---|
Your shell (message in the iframe) | The Admin parent window that posts data, theme, and style | Admin origins for your region (for example https://admin.tago.io), not the API host |
Admin host (message on the parent) | Your iframe document origin | The API host that serves GET /dashboard/{id}/html |
Admin posts into the iframe with the API origin as the postMessage target so the browser only delivers to that document. The sender origin on those messages is still Admin. If your shell allowlists only the API host, every legitimate dashboard:data, dashboard:theme, and dashboard:style message is dropped.
Ignore messages from origins you do not expect.
Rendering data
- TagoSQL:
datafollows the query result shape for that execution (columns and rows when the API returns them that way). See Executing Queries. - API GET:
datais whatever the server returns after the GET (parsed as provided by the host). - None: still handle
dashboard:datawithsource: nullif you want a single code path; use theme for light and dark styling.
Validate and narrow types in your own code. Do not assume a fixed schema beyond { source, data }.
What not to do
- Do not expect the custom-widget SDK handshake or realtime variable streams. That is a different product surface; see Custom Dashboard vs Custom Widget.
- Do not embed profile tokens in the HTML file. Data is injected by the host after ready.
- Do not rely on an in-app editor: download, edit, upload (HTML shell).
Sandbox
Admin loads the shell in an iframe with a restricted sandbox (allow-scripts and allow-same-origin in the
current host). Design for that environment: no parent DOM access beyond postMessage, and careful use of
storage and navigation APIs.