---
title: Iframe blocker
description: Hold YouTube videos, maps and other embeds in a JavaScript app
  until their consent category is allowed, with data-src iframes and the c15t
  iframe blocker in @c15t/browser, createConsentRuntime or your own kernel.
group: frameworks
lastModified: "2026-10-10T16:01:45+01:00"
---
## Gate an embed

Put the embed's URL in `data-src` instead of `src`, and name its category in
`data-category`:

```html title="index.html"
<iframe
	data-src="https://www.youtube-nocookie.com/embed/czTksCF6X8Y?playsinline=1"
	data-category="measurement"
	title="YouTube video"
	allow="encrypted-media; picture-in-picture"
	allowfullscreen
></iframe>
```

The iframe blocker gives the iframe its `src` once the category is allowed,
and takes it away again when the visitor withdraws it. An iframe without
`src` loads nothing, so the vendor gets no request before consent.
`@c15t/browser` and `createConsentRuntime` run the blocker by default.

## Attributes

|Attribute|What it does|
|--|--|
|`data-src`|The embed's URL. Only `http:` and `https:` URLs load. Relative URLs resolve against the page.|
|`data-category`|One category name. An unknown name logs a warning and keeps the iframe blocked.|
|`data-vendor`|Optional vendor slug. The iframe also stays blocked while the visitor has turned that vendor off.|
|`data-c15t-paused`|Set by c15t when it removed a `src` the iframe already had.|

Iframes without `data-category` or `data-vendor` are left alone. An iframe
written with `src` and `data-category` starts loading before the blocker
runs, so always use `data-src`.

## Iframes your code adds

The blocker watches the whole document for new iframes and for changes to
`data-category` and `data-vendor`. An iframe your app renders after load is
gated as soon as it is inserted, before the browser fetches it, as long as it
is inserted with `data-src` and no `src`. Watching the document root keeps the
blocker working when a client router such as Turbo replaces `<body>`, and lets
it start from a script in `<head>` before `<body>` exists.

To render the embed yourself instead, skip the attributes and create the
iframe when `snapshot.effectivePermissions.measurement` becomes `true`. The
[headless example](/docs/frameworks/javascript/headless) does this for its
YouTube video.

## Show a placeholder

c15t draws nothing in place of a blocked iframe. Show your own message beside
it and hide it once the iframe has a `src`:

```css
iframe[data-category]:not([src]) { display: none; }
iframe[src] + .placeholder { display: none; }
```

Give the placeholder a button that calls `consent.openDialog()`, so the
visitor can allow the category.

## Configure it

|`iframeBlocker` value|Effect|
|--|--|
|omitted or `{}`|Gate every iframe with `data-category` or `data-vendor`.|
|`{ disableAutomaticBlocking: true }`|Do not scan or watch the page. c15t checks iframes only when you call `processIframes()`. See [check iframes on demand](#check-iframes-on-demand).|
|`false`|No blocker. `data-src` iframes never load.|

The categories of gated iframes are added to the categories the preference
dialog offers.

## Check iframes on demand

With `iframeBlocker: { disableAutomaticBlocking: true }`, the blocker does
nothing until you call `processIframes()`. Each call pauses gated iframes,
those with `data-category` or `data-vendor`, that consent does not allow, and
restores the ones it does. Call it after you add iframes and after consent
changes:

```ts
consent.on('consent', () => consent.processIframes());
```

`consent.processIframes()` is on the `@c15t/browser` client, and
`runtime.processIframes()` on a runtime from `createConsentRuntime`. The call
does nothing before `start()`, after `dispose()`, or when `iframeBlocker` is
`false`. With automatic blocking on, the blocker does this by itself.

## Attach it to your own kernel

A kernel from `createConsentKernel` has no iframe blocker. Create one for
it:

```ts title="src/iframe-blocker.ts"
import type { ConsentKernel } from 'c15t';
import { createIframeBlocker } from 'c15t/modules/iframe-blocker';

// Watches the page for <iframe data-src data-category> and sets `src` while
// the category is allowed.
export const gateIframes = function gateIframes(kernel: ConsentKernel) {
	return createIframeBlocker({ kernel });
};
```

`createIframeBlocker` returns `{ dispose, processAllIframes }`.
`processAllIframes()` scans the page again and applies the current consent.
`dispose()` stops watching and leaves the iframes as they are.

## Check it works

1. Before a choice, the iframe has no `src` and the Network tab shows no
   request to the embed's host.
2. Allow the category. The iframe gets its `src` and loads.
3. Add an iframe with `data-src` from the console after the page loaded. It
   loads only while its category is allowed.
