Skip to main content

Astro Components

ConsentBanner

Add the banner to your layout

Render one ConsentBanner in the layout that wraps every page, after your page content:

src/layouts/base.astro
---
import { ClientRouter } from 'astro:transitions';
import {
	ConsentBanner,
	ConsentDialog,
	ConsentDialogLink,
	ConsentScript,
} from 'c15t/astro/components';

interface Props {
	title: string;
}

const { title } = Astro.props;
---

<html lang="en">
	<head>
		<meta charset="utf-8" />
		<meta content="width=device-width, initial-scale=1" name="viewport" />
		<title>{title}</title>
		<ConsentScript />
		<ClientRouter />
	</head>
	<body>
		<slot />
		<footer>
			<ConsentDialogLink>Privacy settings</ConsentDialogLink>
		</footer>
		<ConsentBanner />
		<ConsentDialog />
	</body>
</html>

ConsentBanner renders on the server from Astro.locals.c15t. It ships no framework JavaScript. A small shared script adds one click listener to the document, and that listener turns the banner's buttons into consent actions.

When the banner renders

The server renders the banner markup only when the resolved policy asks for a banner and this visitor has not answered yet:

  • A returning visitor who has chosen gets no banner markup at all.
  • A rule with prompt: 'none' renders no banner.
  • With no resolved policy, because the backend failed, timed out or matched no rule, the server renders a hidden placeholder. The browser renders the banner into it if its own /init request resolves a policy.

On a prerendered page, the same HTML serves every visitor, so the banner ships hidden. A small inline script right after it shows the banner at first paint when the visitor has no stored consent. For a visitor with a stored choice, the consent runtime decides once it has read the cookie. In hosted() and manifest() modes, the build cannot know the policy, so the page carries the placeholder and the browser renders the banner after /init. See Rendering and deployment.

After a save, the browser hides the banner by setting the hidden attribute. After a ClientRouter navigation, it shows or hides the new page's banner to match the visitor's state.

Props

PropTypeDefaultEffect
titlestringcookieBanner.title, or cookieBanner.noticeTitle under a noticeBanner heading
descriptionstringcookieBanner.description, or cookieBanner.noticeDescription under a noticeBanner body text
acceptButtonTextstringcommon.acceptAllAccept button label
rejectButtonTextstringcommon.rejectAllReject button label
customizeButtonTextstringcommon.customizeCustomize button label
dismissButtonTextstringcommon.acknowledgeLabel of the button that acknowledges a notice
legalLinks('privacyPolicy' | 'cookiePolicy' | 'termsOfService')[] | nullNoneWhich links from the integration's legalLinks render after the description
hideBrandingbooleanfalseRemoves the "Secured by" tag
noStylebooleanfalseRenders the markup without c15t's class names or button styles
classstringNoneExtra class on the banner root, kept with noStyle
forcebooleanfalseRenders the banner even when the server decided to hide it, for visual tests
noncestringAstro.locals.c15t.nonceContent Security Policy nonce for this banner's inline scripts and style. See Content Security Policy

A prop wins over the translation for the visitor's language, which wins over the English default. For wording on every page, set i18n.messages in the integration instead. See Translations.

force does not create a policy. With no resolved policy, or under a rule that owes no consent interface, the banner still renders nothing. On a prerendered page, force also renders the banner visible.

Actions come from the policy

The resolved policy rule decides which buttons the banner shows and in which order. The same resolver runs for the React, Vue and Svelte banners, so a rule produces the same actions on every framework:

  • A choice prompt shows Reject and Accept at equal prominence, plus Customize when the rule offers it. Customize is the primary action by default.
  • A notice prompt shows an acknowledgement button and, under an opt-out rule, a "Do not sell or share my data" button. The acknowledgement records that the visitor saw the notice. It records no consent and changes no permission.
  • A rule that keeps preferences reachable adds a "Manage preferences" button.

Every button that opens preferences opens the preference dialog. None of them submits an opt-out by itself. How consent works explains prompts, rights and recorded choices.

Shape and position

The integration's presentation.prompt option sets the banner's variant and position for every page. The policy still decides the actions:

VariantPositionsDefault position
floating (default)bottom-left, bottom-right, top-left, top-right, bottom-center, top-centerbottom-left
bartop, bottombottom
widgetbottom-left, bottom-right, top-left, top-rightbottom-right
wallcentercenter

A default corner mirrors left and right for right-to-left languages. A position you set is never mirrored. A choice wall always blocks the page. A notice never blocks, and asking for a notice wall falls back to floating. See change the banner's shape and position and the design gallery.

The banner has no per-page variant or layout prop. Every banner on the site shares presentation.prompt.

Define the links once in the integration's legalLinks option, then pick which ones this banner shows:

src/layouts/base.astro (partial)
<ConsentBanner legalLinks={['privacyPolicy', 'cookiePolicy']} />

A key renders only when the integration defines it. Each link opens in a new tab. A link without a label reads as the translated name for its type, such as "Privacy Policy" in English or "Datenschutzerklärung" in German. Set label in the integration options, such as { href: '/privacy', label: 'Privacy notice' }, to use your own wording. ConsentDialog takes the same legalLinks list.

Accessibility

  • The card has role="region" and an aria-label set to the banner title. It leaves the page usable by keyboard and pointer.
  • A blocking banner, such as a choice wall, has role="dialog" and aria-modal="true". The browser locks page scroll, traps focus inside the card and shows a backdrop until the visitor answers.
  • The root carries lang and dir for the translation's language, so screen readers pronounce the copy correctly and right-to-left text lays out correctly.
  • Every action is a <button type="button">. Until the consent runtime starts, a click does nothing, because the page has not loaded the handler yet.

Style the banner

Theme tokens and theme.consentActions change colors, type, radius and button styles without touching the markup. See Customize.

To write your own CSS, target the attributes. They stay with noStyle:

ElementAttributes
Rootdata-testid="consent-banner-root", data-prompt (choice or notice), data-model, data-variant, data-position, data-blocking, data-c15t-visible
Backdropdata-testid="consent-banner-overlay", rendered only for a blocking banner
Carddata-testid="consent-banner-card"
Title and descriptiondata-testid="consent-banner-title", data-testid="consent-banner-description"
Footer and button groupsdata-testid="consent-banner-footer", data-direction, data-split, data-fill
Each actiondata-action (accept, reject, customize or dismiss), data-testid="consent-banner-<action>-button"
Each rights buttondata-action="right", data-right (opt-out or preferences)
Each legal linkdata-testid="consent-banner-legal-link-<key>"

Keep c15t's rules loaded when you restyle the banner. The browser hides the banner with the hidden attribute, and c15t's rules make hidden win over the banner's display rule.

Next steps