---
title: ConsentGate
description: Gate a YouTube or map iframe behind consent with ConsentGate in
  React, showing a placeholder that opens the preference center until the
  category is allowed.
group: frameworks
lastModified: "2026-10-10T16:01:45+01:00"
---
## Gate an embed behind consent

`ConsentGate` mounts its children only while the effective permission for one
consent category is granted, and shows a placeholder otherwise. Wrap any
iframe or third-party widget that would set cookies or contact a vendor on
load. Render it anywhere inside the `ConsentProvider` from your
[quickstart](../quickstart), which also mounts the `ConsentDialog` the
placeholder button opens.

```tsx title="src/product-video.tsx"
import { ConsentGate } from 'c15t/react';

export function ProductVideo() {
	return (
		<ConsentGate category="marketing" className="video-frame">
			<iframe
				src="https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ"
				title="Product tour"
				loading="lazy"
				allowFullScreen
				style={{ width: '100%', aspectRatio: '16 / 9', border: 0 }}
			/>
		</ConsentGate>
	);
}
```

`marketing` must be in your policy scope. If you set a non-empty
`options.consentCategories` list on `ConsentProvider`, include `marketing`
there too so the preference center can display and save it. `ConsentGate` does
not add categories to that list.

The placeholder names the category using its title from your translations. Set `className` or `style`
on `ConsentGate` to reserve the embed's space, so the page does not shift when the
placeholder is replaced.

## Props

|Prop|Type|Default|Description|
|--|--|--|--|
|`category`|`AllConsentNames`|required|The consent category whose effective permission gates the children, for example `'marketing'` or `'functionality'`.|
|`children`|`ReactNode`|required|The embed. It is not mounted until the category is allowed, so it makes no requests before permission.|
|`placeholder`|`ReactNode`|built-in placeholder|Replaces the built-in title and button while the category is not allowed. Falsy values such as `null`, `false`, `0` and an empty string use the built-in placeholder. Pass an empty fragment, `<></>`, to render no placeholder content.|

Other `div` attributes such as `className`, `style` and `ref` apply to the
wrapper element that stays in the page in both states. `ConsentGateProps` also
declares `noStyle` and `theme`, but the component does not apply them yet;
style the wrapper with `className`, and style the built-in placeholder
through its parts in the provider options.

## Behavior

`ConsentGate` renders a `div` wrapper. Inside it, when the effective permission for
`category` is granted, it renders the children; otherwise it renders the
placeholder. Permission is the same value `useConsent(category)` returns,
so it can be granted under an opt-out rule before the visitor records a
choice, and a recorded grant can be overridden by a privacy signal.

While effective permission is denied, the children are absent from the DOM,
so an iframe or a third-party widget inside `ConsentGate` sends no requests. When the visitor later revokes it, the
children unmount and the embed disappears. `ConsentGate` does not depend on the
network blocker or the iframe blocker modules; use those for markup you
cannot wrap in a component.

The built-in placeholder shows the title `consentGate.title`,
`Accept {category} consent to view this content.`, with `{category}`
replaced by `consentTypes.<category>.title`, and a button labelled
`consentGate.actionButton`, `Enable {category} consent`. The button opens the
preference center; it
does not grant the category by itself, because the visitor still has to
save. Mount a `ConsentDialog` in the same provider so the button has
something to open.

Under a policy with `scopeMode: 'strict'`, a category outside the rule's
scope cannot be granted. The built-in placeholder then shows `consentGate.policyBlocked`,
"This content is unavailable under your region's consent policy.", without
the button, and a stored grant for that category stays blocked.

When the provider starts from a snapshot prefetched on the server, the
server renders the placeholder for a denied category. For a granted
category it renders the empty wrapper, and the children mount once
hydration completes, so an iframe loads once even when the page streams
inside a `Suspense` boundary. A grant in the request cookie allows the
embed only when policy and privacy signals permit it. Browser privacy signals detected during hydration can
withdraw that permission. In a browser-only setup the
provider has no permission until it resolves policy on the client, so
`ConsentGate` shows the placeholder first and swaps in the embed after resolution
when the category is already granted.

`ConsentGate` is one component on this page; the per-vendor embed guides for
[YouTube](/docs/integrations/youtube) and
[Google Maps](/docs/integrations/google-maps) show a complete embed
configuration with sizing, titles and the same `ConsentGate` usage across
frameworks.

To read the same permission in your own components, use `useConsent` from
`c15t/react`; the `useIframeBlocker` and `useNetworkBlocker` module hooks
also import from there.

## Composition

The placeholder parts are available as `ConsentGate.Root`, `ConsentGate.Title` and
`ConsentGate.Button` for a custom placeholder that keeps the built-in copy and
behavior. `ConsentGate.Title` and `ConsentGate.Button` accept `category` and fill in the
translated text; pass children to either to replace it.

```tsx
<ConsentGate
  category="marketing"
  placeholder={
    <ConsentGate.Root>
      <ConsentGate.Title category="marketing" />
      <p>The video is also available on our channel.</p>
      <ConsentGate.Button category="marketing">Choose cookies</ConsentGate.Button>
    </ConsentGate.Root>
  }
>
  <iframe src="https://www.youtube-nocookie.com/embed/..." title="Product tour" />
</ConsentGate>
```

To restyle the built-in placeholder without replacing it, set its parts
under `components['consent-gate']` in the provider options. `root` is the
card, and `title` and `button` are its text and button. The `consentGate`, `consentGateTitle` and
`consentGateButton` theme slots reach the same parts. The button part
applies on top of `button.primary`.

```tsx
components: {
  'consent-gate': {
    root: { className: 'rounded-none' },
    button: { className: 'font-semibold' },
  },
},
```

The built-in placeholder carries `data-testid="consent-gate-placeholder"`,
its title `data-testid="consent-gate-title"` and its button
`data-testid="consent-gate-button"`.

## Accessibility

The placeholder is plain text and a `button`, so it is readable and
operable without the embed. Give the iframe a descriptive `title`, and keep a
transcript, address or link outside the `ConsentGate` for visitors who decline
the category. A denied category may be fixed by policy, so opening the
preference center does not guarantee that the visitor can grant it.

## Verify

Use an optional in-scope category that is available in the preference
center and is not restricted by policy or privacy signals such as GPC.
Load the page with the category denied. The placeholder text names the
category and the network panel shows no request to the embed's host. With a
prefetched snapshot, the server HTML contains the placeholder for a denied
category and neither placeholder nor embed for a granted category; the
embed appears after hydration and the network panel shows one request for
its document. Browser-only initialization
shows the placeholder before policy resolves, then replaces it with the
embed if the category is granted. After policy resolves, activate the
placeholder button: the preference center opens. Turn the category on and
Save: the placeholder is replaced by the embed and its requests start. Reject the
category again from the preference center: the embed disappears.
