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

AttributeRequiredDefaultNotes
dashboard-idThe trend dashboard UUID (from the dashboard’s ellipsis menu once Embed is on)
tokenThe JWE minted via POST /api/embed/token
base-urlAuto-detected from script srcOverride if you self-host or use a staging URL
height600pxAny CSS length
width100%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 &lt;trend-dashboard&gt; 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 to app.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 the Referer header.

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.