Next.js Components
ConsentGate
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. It is a Client Component; render it from any route under the layout
that mounts the ConsentRoot from your
App Router or
Pages Router setup, which also mounts
the ConsentDialog the placeholder button opens.
Import ProductVideo into any page or Server Component; only this file needs
'use client'. marketing must be in your policy scope. ConsentGate
registers its category when it mounts, so the preference center lists
Marketing even if you set options.consentCategories without it.
ConsentGate does not read the clock during server rendering, so a page that
renders it can still be prerendered, including with cacheComponents: true.
It does not need a Suspense boundary of its own. On a static page the
prerendered HTML contains the placeholder, and the embed replaces it after
the browser resolves policy and the category is allowed.
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.
With the default App Router layout, which passes the pending
resolveConsent result, the server HTML contains the placeholder, and the
browser swaps in the embed after it applies the resolved state if the
category is granted. No request reaches the embed's host before that.
With the
awaited layout,
the server decides from the resolved rule and the request cookie whether the
category is allowed. Pages Router works the same way when getServerSideProps
awaits resolveConsent from c15t/next/pages and passes the result
through state to ConsentRoot, as in the
Pages Router guide. A denied category gets the placeholder
in the server HTML. A granted category gets an empty wrapper, and the embed
mounts once hydration completes. With the awaited layout the page streams
inside a Suspense boundary, and React moves streamed HTML into place after
parsing it; an iframe that was already in that HTML would load twice.
Hydration keeps the server's decision as long as the browser's privacy
signals agree with the request: a navigator.globalPrivacyControl
that the request did not carry as Sec-GPC is re-detected on hydration and
can withdraw a category. Under an opt-in rule a new
visitor gets the placeholder and a returning visitor who allowed marketing
gets the iframe; under an opt-out rule a new visitor already has effective
permission, so the iframe renders until they opt out.
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 and
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 Client Components, use useConsent
from c15t/next; 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.
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.
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.