Web component mode
The framework-agnostic trend-dashboard custom element for SPAs and beyond.
Everything the iframe mode does, in a framework-agnostic custom element you can drop into React, Vue, Svelte, Angular, or plain HTML.
Include the script
Load once, anywhere on your page:
<script src="https://app.trend.dev/embed.js" defer></script>
Then use the element:
<trend-dashboard
dashboard-id="<uuid>"
token="<jwe from your backend>"
></trend-dashboard>
The script is ~6KB uncompressed, has zero dependencies, and registers a <trend-dashboard> custom element on first load. It’s idempotent — loading it twice is safe.
Attributes
| Attribute | Required | Default | Notes |
|---|---|---|---|
dashboard-id | ✅ | — | The trend dashboard UUID (from the dashboard’s ellipsis menu once Embed is on) |
token | ✅ | — | The JWE minted via POST /api/embed/token |
base-url | Auto-detected from script src | Override if you self-host or use a staging URL | |
height | 600px | Any CSS length | |
width | 100% | Any CSS length |
Attribute changes are live — setting a new token value swaps the iframe src in place without unmounting the frame. Useful for refresh-before-expiry flows.
Framework interop
React (19+)
React 19 forwards unknown props as attributes on custom elements, so <trend-dashboard> works natively:
export function CustomerDashboard({ token, dashboardId }) {
return (
<trend-dashboard
dashboard-id={dashboardId}
token={token}
height="700px"
/>
)
}
For React 18 and older, use ref + setAttribute:
import { useEffect, useRef } from 'react'
export function CustomerDashboard({ token, dashboardId }) {
const ref = useRef(null)
useEffect(() => {
ref.current?.setAttribute('token', token)
ref.current?.setAttribute('dashboard-id', dashboardId)
}, [token, dashboardId])
return <trend-dashboard ref={ref} />
}
Vue
Vue has forwarded custom-element attributes since 2.6:
<template>
<trend-dashboard :dashboard-id="dashboardId" :token="token" height="700px" />
</template>
Svelte / Angular / plain HTML
All work — custom elements are DOM-standard. No wrapper required.
Copy-paste smoke test
Save this as trend-embed-test.html anywhere on your disk. Swap the two placeholders (dashboard UUID + token from Embed → API keys → Try it in-app → Mint a test token), then double-click the file to open in a browser (or serve it from any static server):
<!doctype html>
<html>
<head>
<title>trend embed smoke test</title>
<style>
body { font-family: system-ui; max-width: 900px; margin: 40px auto; padding: 0 16px; }
.frame-wrap { border: 1px solid #e5e5e5; border-radius: 8px; overflow: hidden; margin-bottom: 24px; }
button { padding: 8px 12px; font-size: 14px; cursor: pointer; margin-right: 8px; }
</style>
</head>
<body>
<h1>trend <trend-dashboard> smoke test</h1>
<div class="frame-wrap">
<trend-dashboard
id="td"
dashboard-id="PASTE_DASHBOARD_UUID_HERE"
token="PASTE_TOKEN_HERE"
height="700px"
></trend-dashboard>
</div>
<button onclick="document.getElementById('td').setAttribute('height', '400px')">
Resize to 400px
</button>
<button onclick="document.getElementById('td').setAttribute('token', 'obviously-invalid')">
Break the token (should show error)
</button>
<button onclick="location.reload()">Reload</button>
<script src="https://app.trend.dev/embed.js" defer></script>
</body>
</html>
If the dashboard renders and the "Resize" + "Break the token" buttons update the iframe in place, you’re fully wired up.
Iframe sandbox
The web component sets these on the iframe by default:
sandbox="allow-scripts allow-same-origin"— scripts run (needed for the SPA to render), same-origin fetches work (needed for chart queries toapp.trend.dev), but forms, popups, and top-navigation are blocked.referrerpolicy="no-referrer"— when a customer clicks an outbound link inside the iframe, the parent URL (which may contain?token=) never leaks to the destination via theRefererheader.
Token lifetime
Same 1-hour lifetime as iframe mode. For long-lived embed sessions:
// Every 50 minutes, ask your backend for a fresh token and swap it in.
setInterval(async () => {
const res = await fetch('/api/my-app-endpoint/mint-trend-token')
const { token } = await res.json()
document.querySelector('trend-dashboard').setAttribute('token', token)
}, 50 * 60 * 1000)
The iframe reloads with the new token; the parent element stays mounted.
Driving filters from the parent app
Two ways to update dashboard variables (control values) from the outside:
Set once at mount — the variables attribute serializes into URL params on the initial load:
<trend-dashboard
dashboard-id="…"
token="…"
variables='{"country": "US", "tiers": ["pro", "premium"]}'
></trend-dashboard>
Update live — call setVariable(name, value) on the element:
const el = document.querySelector('trend-dashboard')
el.setVariable('country', 'CA')
// or force every chart to re-fetch:
el.refresh()
Both go through postMessage to the embedded viewer without reloading the iframe. The dashboard reacts as if the customer had changed the control themselves.
Listening for load / error events
The custom element dispatches DOM CustomEvents so the parent app can hide its own loading state or show a fallback:
const el = document.querySelector('trend-dashboard')
el.addEventListener('trend-loaded', () => {
// Dashboard finished rendering.
})
el.addEventListener('trend-error', (e) => {
console.error(e.detail.message)
})
Demo customers (free, watermarked)
Any customer toggled to Demo in Embed → Customers → Registered is excluded from billing AND renders with a diagonal "DEMO" watermark across every chart. Same watermark, same billing exclusion as iframe mode — the JWT carries the demo flag regardless of which surface renders it. Details: Overview → Pricing.
What to read next
- Per-customer attributes — the
{{trend.attributes.*}}safety semantics. - iframe mode — the plain-
<iframe>alternative.