> ## Documentation Index
> Fetch the complete documentation index at: https://lightdash-mintlify-9d6f9427.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Embedding with iframe

> Complete reference for embedding Lightdash content using iframes with JWTs in URL hash fragments

<Info>
  Embedding is available to all Lightdash Cloud users and Enterprise On-Prem customers. [Get in touch](https://lightdash.typeform.com/to/BujU5wg5) to have this feature enabled in your account.
</Info>

## Overview

iframe embedding is the simplest way to embed Lightdash **dashboards** and standalone **data apps** in your application. It requires no special libraries, dependencies, or CORS configuration—just generate a JWT and construct an embed URL.

<Warning>
  iframe embedding supports dashboards and [data apps](/guides/embedding/data-apps). Chart embedding requires the [React SDK](/references/react-sdk).
</Warning>

### Benefits of iframe embedding

* **Simple integration** - Standard HTML iframe element, works anywhere
* **No dependencies** - No JavaScript libraries or SDK installation required
* **No CORS configuration** - Unlike the React SDK, iframes don't require CORS setup
* **Universal compatibility** - Works in any web environment (React, Vue, Angular, vanilla HTML)
* **Secure** - JWT in URL hash fragment isn't sent to server or logged

### When to use iframe embedding

* Quick integration without adding dependencies
* Non-React applications
* Content management systems (WordPress, Webflow, etc.)
* Simple HTML pages or static sites
* When you don't need programmatic control (filters, callbacks)

### When to use React SDK instead

Consider the [React SDK](/references/react-sdk) if you need:

* Programmatic filters (apply filters via props)
* Callbacks (e.g., onExplore for analytics)
* Seamless React integration
* TypeScript type definitions

For JWT structure and configuration options, see the [embedding reference](/references/embedding).

## iframe URL patterns

All embed URLs follow this pattern: `https://your-instance.lightdash.cloud/embed/{projectUuid}#{jwtToken}`

The JWT is passed in the URL **hash fragment** (`#token`) for security—it's not sent to the server in requests or logged in browser history.

### Dashboard URL

```
https://your-instance.lightdash.cloud/embed/{projectUuid}#{jwtToken}
```

The `dashboardUuid` is specified inside the JWT payload as `content.dashboardUuid`, not in the URL path.

**Example:**

```
https://app.lightdash.cloud/embed/abc-123-def-456#eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```

### Data app URL

Standalone [data app](/guides/embedding/data-apps) embeds put both the project UUID and the app UUID in the path:

```
https://your-instance.lightdash.cloud/embed/{projectUuid}/app/{appUuid}#{jwtToken}
```

The JWT must use `content.type: 'dataApp'` and the same `appUuid` as the URL. A token for one app cannot render any other app.

**Example:**

```
https://app.lightdash.cloud/embed/abc-123-def-456/app/app-uuid-789#eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```

<Info>
  The JWT in the hash fragment is NOT sent to the server with HTTP requests and does NOT appear in server logs or browser history, providing an additional security layer.
</Info>

<Warning>
  **Chart embedding via iframe is not currently supported.** Charts can only be embedded using the [React SDK](/references/react-sdk).
</Warning>

## URL construction

### Building the embed URL

1. **Get your project UUID** - Found in Lightdash project settings
2. **Get dashboard ID** - Dashboard UUID or slug
3. **Generate JWT** - See [embedding reference](/references/embedding#jwt-token-structure) for token structure
4. **Construct URL** - Combine parts with hash fragment

<CodeGroup>
  ```javascript Node.js theme={null}
  import jwt from 'jsonwebtoken';

  const LIGHTDASH_EMBED_SECRET = process.env.LIGHTDASH_EMBED_SECRET;
  const instanceUrl = 'https://app.lightdash.cloud';
  const projectUuid = 'your-project-uuid';
  const dashboardUuid = 'your-dashboard-uuid';

  // Generate JWT
  const token = jwt.sign({
    content: {
      type: 'dashboard',
      dashboardUuid: dashboardUuid,
      canExportCsv: true,
    },
  }, LIGHTDASH_EMBED_SECRET, { expiresIn: '1h' });

  // Build embed URL
  const embedUrl = `${instanceUrl}/embed/${projectUuid}#${token}`;

  console.log(embedUrl);
  ```

  ```python Python theme={null}
  import jwt
  import datetime

  LIGHTDASH_EMBED_SECRET = os.getenv('LIGHTDASH_EMBED_SECRET')
  instance_url = 'https://app.lightdash.cloud'
  project_uuid = 'your-project-uuid'
  dashboard_uuid = 'your-dashboard-uuid'

  # Generate JWT
  payload = {
      'content': {
          'type': 'dashboard',
          'dashboardUuid': dashboard_uuid,
          'canExportCsv': True,
      },
      'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1)
  }

  token = jwt.encode(payload, LIGHTDASH_EMBED_SECRET, algorithm='HS256')

  # Build embed URL
  embed_url = f"{instance_url}/embed/{project_uuid}#{token}"

  print(embed_url)
  ```

  ```ruby Ruby theme={null}
  require 'jwt'

  lightdash_embed_secret = ENV['LIGHTDASH_EMBED_SECRET']
  instance_url = 'https://app.lightdash.cloud'
  project_uuid = 'your-project-uuid'
  dashboard_uuid = 'your-dashboard-uuid'

  # Generate JWT
  payload = {
    content: {
      type: 'dashboard',
      dashboardUuid: dashboard_uuid,
      canExportCsv: true
    },
    exp: Time.now.to_i + 3600
  }

  token = JWT.encode(payload, lightdash_embed_secret, 'HS256')

  # Build embed URL
  embed_url = "#{instance_url}/embed/#{project_uuid}##{token}"

  puts embed_url
  ```
</CodeGroup>

### URL with user attributes

For row-level security, include user attributes in the JWT:

```javascript theme={null}
const token = jwt.sign({
  content: {
    type: 'dashboard',
    dashboardUuid: 'your-dashboard-uuid',
  },
  userAttributes: {
    tenant_id: user.tenantId,  // Filter data by tenant
    region: user.region,
  },
}, LIGHTDASH_EMBED_SECRET, { expiresIn: '1h' });

const embedUrl = `https://app.lightdash.cloud/embed/${projectUuid}#${token}`;
```

See [User attributes reference](/references/workspace/user-attributes) for complete guide.

## URL parameters

Embedded dashboards support a few optional URL parameters that customize the visual appearance and the query timezone of the session. These are added as **query parameters** before the hash fragment.

### URL format

```
https://your-instance.lightdash.cloud/embed/{projectUuid}?theme=dark&backgroundColor=1c1c1c&timezone=America%2FLos_Angeles#{jwtToken}
```

### `theme`

Sets the color scheme for the embedded content.

| Value   | Description                  |
| ------- | ---------------------------- |
| `light` | Light color scheme (default) |
| `dark`  | Dark color scheme            |

**Example:**

```
https://app.lightdash.cloud/embed/abc-123?theme=dark#eyJhbGci...
```

### `backgroundColor`

Sets a custom background color on the embed. Accepts bare hex codes **without** the `#` prefix. The `#` is automatically prepended.

| Format      | Example                    | Description    |
| ----------- | -------------------------- | -------------- |
| 6-digit hex | `backgroundColor=121212`   | Full hex color |
| 3-digit hex | `backgroundColor=FFF`      | Shorthand hex  |
| 8-digit hex | `backgroundColor=FF000080` | Hex with alpha |

<Warning>
  Only hex color codes are supported. Other CSS color formats (named colors, `rgb()`, `hsl()`, etc.) are **not** supported.
</Warning>

**Example:**

```
https://app.lightdash.cloud/embed/abc-123?backgroundColor=1c1c1c#eyJhbGci...
```

### `timezone`

<Info>
  **Beta:** The embed `timezone` param is currently in the [Beta](/references/workspace/feature-maturity-levels) phase. Contact support to enable it for your organization.
</Info>

Sets a per-session query timezone for the embed. Use it when one embedded dashboard needs to render in different timezones for different viewers (for example, a multi-tenant app where each tenant has its own timezone), without creating separate charts per timezone.

Accepts an IANA timezone name (e.g. `America/Los_Angeles`, `Europe/Berlin`, `Asia/Tokyo`). The value is used for dashboard tile queries, inherited totals and subtotals, and filter autocomplete in that session.

URL-encode the value so the `/` is escaped (`America%2FLos_Angeles`); the plain `America/Los_Angeles` form is also accepted.

| Format             | Example                        | Description                             |
| ------------------ | ------------------------------ | --------------------------------------- |
| IANA timezone name | `timezone=America/Los_Angeles` | Renders the session in Los Angeles time |
| IANA timezone name | `timezone=Europe/Berlin`       | Renders the session in Berlin time      |

**Example:**

```
https://app.lightdash.cloud/embed/abc-123?timezone=America%2FLos_Angeles#eyJhbGci...
```

<Info>
  The embed `timezone` param sits at the top of the [query timezone hierarchy](/guides/developer/timezones#how-the-project-query-timezone-is-resolved) for that session and overrides any per-chart timezone pin. It only applies to direct iframe and shareable URL embeds, not the React SDK, where the host app's URL is unrelated to the embed.
</Info>

<Note>
  If Beta timezone support is off, the param is ignored and the session falls back to the chart pin or project default. An invalid timezone value returns a 400 error.
</Note>

### Combining parameters

You can use `theme`, `backgroundColor`, and `timezone` together:

```
https://app.lightdash.cloud/embed/abc-123?theme=dark&backgroundColor=1c1c1c&timezone=America%2FLos_Angeles#eyJhbGci...
```

<Info>
  All URL parameters are optional. Existing embed URLs without these parameters behave exactly as before — light theme, default background, and the chart or project query timezone.
</Info>

### Building themed URLs

<CodeGroup>
  ```javascript Node.js theme={null}
  import jwt from 'jsonwebtoken';

  const LIGHTDASH_EMBED_SECRET = process.env.LIGHTDASH_EMBED_SECRET;
  const instanceUrl = 'https://app.lightdash.cloud';
  const projectUuid = 'your-project-uuid';
  const dashboardUuid = 'your-dashboard-uuid';

  const token = jwt.sign({
    content: {
      type: 'dashboard',
      dashboardUuid: dashboardUuid,
    },
  }, LIGHTDASH_EMBED_SECRET, { expiresIn: '1h' });

  // Build embed URL with theming parameters
  const embedUrl = `${instanceUrl}/embed/${projectUuid}?theme=dark&backgroundColor=1c1c1c#${token}`;
  ```

  ```python Python theme={null}
  import jwt
  import datetime

  LIGHTDASH_EMBED_SECRET = os.getenv('LIGHTDASH_EMBED_SECRET')
  instance_url = 'https://app.lightdash.cloud'
  project_uuid = 'your-project-uuid'
  dashboard_uuid = 'your-dashboard-uuid'

  payload = {
      'content': {
          'type': 'dashboard',
          'dashboardUuid': dashboard_uuid,
      },
      'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1)
  }

  token = jwt.encode(payload, LIGHTDASH_EMBED_SECRET, algorithm='HS256')

  # Build embed URL with theming parameters
  embed_url = f"{instance_url}/embed/{project_uuid}?theme=dark&backgroundColor=1c1c1c#{token}"
  ```
</CodeGroup>

## Embedding in HTML

### Basic iframe

The simplest way to embed is with a standard HTML iframe:

```html theme={null}
<iframe
  src="https://app.lightdash.cloud/embed/project-uuid#jwt-token"
  width="100%"
  height="600"
  frameborder="0"
  style="border: none;"
></iframe>
```

### Recommended attributes

```html theme={null}
<iframe
  src="https://app.lightdash.cloud/embed/..."
  width="100%"
  height="600"
  frameborder="0"
  style="border: none;"
  loading="lazy"
  title="Lightdash Dashboard"
  allowfullscreen
></iframe>
```

**Attributes explained:**

* `width="100%"` - Makes iframe responsive to container width
* `height="600"` - Fixed height (adjust based on content)
* `frameborder="0"` - Removes default border (legacy)
* `style="border: none;"` - Removes border (modern CSS)
* `loading="lazy"` - Defers loading until iframe is visible
* `title="..."` - Accessibility: Describes iframe content for screen readers
* `allowfullscreen` - Enables fullscreen mode (if your dashboard uses it)

### Responsive iframes

To make iframes maintain aspect ratio and scale responsively:

**Method 1: Aspect ratio wrapper (16:9)**

```html theme={null}
<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
  <iframe
    src="https://app.lightdash.cloud/embed/..."
    style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: none;"
    frameborder="0"
    allowfullscreen
  ></iframe>
</div>
```

**Method 2: Modern CSS aspect-ratio (16:9)**

```html theme={null}
<iframe
  src="https://app.lightdash.cloud/embed/..."
  style="aspect-ratio: 16/9; width: 100%; border: none;"
  frameborder="0"
  allowfullscreen
></iframe>
```

**Method 3: Fixed viewport percentage**

```html theme={null}
<iframe
  src="https://app.lightdash.cloud/embed/..."
  style="width: 100%; height: 80vh; border: none;"
  frameborder="0"
></iframe>
```

### Dynamic height

iframes have fixed height by default. For dynamic height based on content:

<Warning>
  Lightdash embeds do not currently support automatic height adjustment via postMessage. Use a fixed height or viewport-based height (e.g., `80vh`).
</Warning>

Recommended approach for dashboards with unknown content:

```html theme={null}
<iframe
  src="https://app.lightdash.cloud/embed/..."
  style="width: 100%; min-height: 600px; height: 90vh; border: none;"
  frameborder="0"
></iframe>
```

### Security considerations

iframes provide natural security isolation, but you can add additional restrictions:

```html theme={null}
<iframe
  src="https://app.lightdash.cloud/embed/..."
  sandbox="allow-scripts allow-same-origin allow-forms allow-downloads"
  style="width: 100%; height: 600px; border: none;"
></iframe>
```

**sandbox attributes:**

* `allow-scripts` - Required for Lightdash to function
* `allow-same-origin` - Required for Lightdash to function
* `allow-forms` - Required for filter interactions
* `allow-downloads` - Required if you enable CSV/image exports

<Info>
  The `sandbox` attribute provides additional security but may restrict functionality. Test thoroughly if you use it.
</Info>

## Common patterns

### Server-side rendering

Generate embed URLs in your server-side templates:

**Express (Node.js)**

```javascript theme={null}
app.get('/dashboard', authenticateUser, async (req, res) => {
  const user = await getUser(req.user.id);

  const token = jwt.sign({
    content: {
      type: 'dashboard',
      dashboardUuid: 'dashboard-uuid',
    },
    userAttributes: {
      tenant_id: user.tenantId,
    },
  }, process.env.LIGHTDASH_EMBED_SECRET, { expiresIn: '1h' });

  const embedUrl = `https://app.lightdash.cloud/embed/${projectUuid}#${token}`;

  res.render('dashboard', { embedUrl });
});
```

**Template (EJS)**

```html theme={null}
<div class="dashboard-container">
  <iframe
    src="<%= embedUrl %>"
    width="100%"
    height="600"
    frameborder="0"
    style="border: none;"
  ></iframe>
</div>
```

### Single-page apps (SPA)

Generate URLs via API when component mounts:

**React example**

```jsx theme={null}
function EmbeddedDashboard() {
  const [embedUrl, setEmbedUrl] = useState(null);

  useEffect(() => {
    fetch('/api/dashboard-embed-url')
      .then(res => res.json())
      .then(data => setEmbedUrl(data.url));
  }, []);

  if (!embedUrl) return <div>Loading...</div>;

  return (
    <iframe
      src={embedUrl}
      width="100%"
      height="600"
      frameBorder="0"
      style={{ border: 'none' }}
    />
  );
}
```

### Static sites

For static sites, generate URLs at build time or use edge functions:

**Next.js (server component)**

```tsx theme={null}
import jwt from 'jsonwebtoken';

async function DashboardPage() {
  // Generate at request time
  const token = jwt.sign({
    content: {
      type: 'dashboard',
      dashboardUuid: 'dashboard-uuid',
    },
  }, process.env.LIGHTDASH_EMBED_SECRET, { expiresIn: '24h' });

  const embedUrl = `https://app.lightdash.cloud/embed/project-uuid#${token}`;

  return (
    <iframe
      src={embedUrl}
      width="100%"
      height="600"
      frameBorder="0"
      style={{ border: 'none' }}
    />
  );
}
```

### Content management systems

Embed in WordPress, Webflow, or other CMS:

1. Create a server endpoint that generates embed URLs
2. Use iframe embed code with dynamic URL
3. Refresh tokens via JavaScript when expired

**WordPress shortcode example:**

```php theme={null}
function lightdash_embed_shortcode($atts) {
    $atts = shortcode_atts(array(
        'dashboard' => '',
    ), $atts);

    $embed_url = generate_lightdash_url($atts['dashboard']);

    return '<iframe src="' . esc_url($embed_url) . '" width="100%" height="600" frameborder="0" style="border: none;"></iframe>';
}
add_shortcode('lightdash', 'lightdash_embed_shortcode');
```

## Token refresh

JWTs expire after the time specified in `expiresIn`. Handle token expiration:

### Option 1: Long-lived tokens

For public or semi-public dashboards, use longer expiration:

```javascript theme={null}
jwt.sign(payload, secret, { expiresIn: '7d' })  // 7 days
```

<Warning>
  Long-lived tokens are convenient but less secure. Use only when appropriate for your use case.
</Warning>

### Option 2: Regenerate URL on expiration

Detect when iframe shows "Token expired" error and reload with new URL:

```javascript theme={null}
function refreshEmbed() {
  fetch('/api/dashboard-embed-url')
    .then(res => res.json())
    .then(data => {
      document.getElementById('dashboard-iframe').src = data.url;
    });
}

// Refresh before expiration (e.g., every 50 minutes for 1-hour tokens)
setInterval(refreshEmbed, 50 * 60 * 1000);
```

### Option 3: Backend proxy

Create a backend endpoint that serves a static iframe URL but generates fresh tokens:

```javascript theme={null}
app.get('/embed-proxy/dashboard/:dashboardUuid', authenticateUser, (req, res) => {
  const token = jwt.sign({
    content: {
      type: 'dashboard',
      dashboardUuid: req.params.dashboardUuid,
    },
  }, process.env.LIGHTDASH_EMBED_SECRET, { expiresIn: '1h' });

  const embedUrl = `https://app.lightdash.cloud/embed/${projectUuid}#${token}`;

  // Redirect to actual embed URL
  res.redirect(embedUrl);
});
```

Then use:

```html theme={null}
<iframe src="/embed-proxy/dashboard/dashboard-uuid"></iframe>
```

## Troubleshooting

### Token not working

**Issue:** iframe shows "Invalid token" or "Token expired"

**Solutions:**

* Verify embed secret matches between token generation and Lightdash
* Check token hasn't expired (`expiresIn` in jwt.sign)
* Ensure JWT payload structure matches [embedding reference](/references/embedding#jwt-token-structure)
* Test token expiration: `jwt.decode(token)` and check `exp` field

### Content not displaying

**Issue:** iframe is blank or shows loading indefinitely

**Solutions:**

* Check browser console for errors
* Verify dashboard/chart UUID is correct
* Ensure content is added to "allowed dashboards/charts" in Lightdash settings
* Check project UUID is correct
* Try accessing embed URL directly in browser to see error message

### CORS errors

**Issue:** Browser console shows CORS errors

**Solution:**

* iframes should NOT have CORS issues (CORS only affects React SDK)
* If you see CORS errors with iframes, you may be using fetch/XHR to load content instead of iframe
* Use standard iframe src attribute, not JavaScript-based loading

### URL encoding issues

**Issue:** JWT or URL appears malformed

**Solutions:**

* Don't URL-encode the JWT in the hash fragment
* If constructing URLs in templates, ensure proper escaping:
  ```html theme={null}
  <!-- Good -->
  <iframe src="https://app.lightdash.cloud/embed/...#<%= token %>"></iframe>

  <!-- Bad - Don't URL encode token -->
  <iframe src="https://app.lightdash.cloud/embed/...#<%= encodeURIComponent(token) %>"></iframe>
  ```

### Dashboard filters not working

**Issue:** Users can't interact with filters despite `dashboardFiltersInteractivity: { enabled: 'all' }`

**Solutions:**

* Verify JWT includes correct interactivity settings
* Check browser console for JavaScript errors
* Ensure iframe isn't using `sandbox` attribute without `allow-forms`

## See also

<CardGroup cols={2}>
  <Card title="Embedding reference" icon="book" href="/references/embedding">
    JWT structure and configuration options
  </Card>

  <Card title="React SDK reference" icon="react" href="/references/react-sdk">
    Alternative: React component embedding
  </Card>

  <Card title="Embedding dashboards" icon="grid-2" href="/guides/embedding/dashboards">
    Step-by-step dashboard embedding guide
  </Card>

  <Card title="Embedding charts" icon="chart-line" href="/guides/embedding/charts">
    Step-by-step chart embedding guide
  </Card>

  <Card title="User attributes" icon="shield-check" href="/references/workspace/user-attributes">
    Row-level security with user attributes
  </Card>
</CardGroup>
