Consent
A consent banner and preference panel driven by the site’s categories, policy version and service configuration.
ConsentMake and revise a choice
Accept, reject or choose optional categories, then reopen settings. Reset clears only the isolated example’s saved choice. No tracking services are configured.
<ConsentPreview /> import fs from 'node:fs/promises'
import path from 'node:path'
import type { SiteConfig, TsSsgContext } from '@purestack/ts-common'
import { defineComponents } from '@purestack/ts-components'
import {
buildConsentScript,
buildThemeSwitchScript,
} from '@purestack/ts-page-scripts'
import { renderApp } from '@purestack/ts-render'
import { getSvgIcon } from '@purestack/ts-svg-icons'
import { withBasePath } from '@purestack/ts-util'
import { defineComponent, html } from 'regor'
export interface ConsentPreview {
previewHref: string
}
const consentPreviewTemplate = html`<iframe
title="Consent isolated example"
class="w-full rounded-md b-1 b-subtle"
style="height: 560px"
:src="previewHref"
></iframe>`
const exampleTemplate = html`<SectionHeader
title="Privacy settings example"
titleTag="h2"
subtitle="Make a choice, then reopen settings to change it."
/>
<div class="consent-settings-teleport-area"></div>
<Consent />
<Btn
variant="outline"
class="mt-3"
type="button"
onclick="
localStorage.removeItem('purestack-docs-consent-example');
location.reload();
"
>
Reset this example
</Btn>`
function createConsentPreviewDocument(site: SiteConfig): string {
const context: TsSsgContext = {
site: { ...site },
pageInfo: {
relPath: 'components/site/consent/consent.mdx',
urlPath: '/components/site/consent/',
frontmatter: {
template: 'doc',
hidden: false,
draft: false,
nav: { hidden: false },
layout: {
showNav: false,
showToc: false,
showFooter: false,
fullWidth: false,
navMode: 'sidebar',
tocCollapsed: false,
},
},
},
theme: site.style.theme,
basePath: site.basePath,
locales: site.i18n.locales,
defaultLocale: site.i18n.defaultLocale,
resolveLocaleHref: () => undefined,
resolvePublicHref: (href) => withBasePath(site.basePath, href),
recordScriptEntrypoint: () => {},
recordRuntimeEmbed: () => {},
}
context.site.consent = {
...context.site.consent,
enabled: true,
storageKey: 'purestack-docs-consent-example',
policyVersion: 'example-1',
bannerTitle: 'Example privacy choices',
bannerDescription: 'This isolated example has no tracking services.',
categories: [
{
id: 'necessary',
label: 'Necessary',
required: true,
description: 'Required for this example’s saved choice.',
},
{
id: 'analytics',
label: 'Analytics',
description: 'Illustrative optional category; no service is loaded.',
},
],
services: [],
}
const body = renderApp(exampleTemplate, {
components: defineComponents(getSvgIcon),
context,
})
const scripts =
buildThemeSwitchScript(['light', 'dark']) +
buildConsentScript(context.site.consent)
const documentHtml = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="robots" content="noindex"><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="stylesheet" href="${withBasePath(site.basePath, '/assets/site.css')}" data-theme="light"><link rel="stylesheet" href="${withBasePath(site.basePath, '/assets/site.dark.css')}" data-theme="dark"></head><body class="template-doc tone--neutral p-3" data-pagefind-ignore="all"><main class="doc-content">${body}</main><script>${scripts}</script></body></html>`
return documentHtml
}
export function defineConsentPreviewComponent(site: SiteConfig) {
return defineComponent<ConsentPreview>(consentPreviewTemplate, {
context: () => ({
previewHref: withBasePath(
site.basePath,
'/components/site/consent/preview.html',
),
}),
})
}
export async function writeConsentPreview(site: SiteConfig) {
const output = path.join(
site.outDir,
'components',
'site',
'consent',
'preview.html',
)
const documentHtml = createConsentPreviewDocument(site)
await fs.mkdir(path.dirname(output), { recursive: true })
await fs.writeFile(output, documentHtml, 'utf8')
} Behavior and accessibility
Consent renders only when site.consent.enabled is true. The runtime opens the banner when no current saved choice exists, persists choices under storageKey and uses policyVersion to identify the policy. Necessary categories remain selected. Keep labels and descriptions specific to the services your site actually runs.
API reference
Consent contract
The three public props style the component. Banner copy, categories, storage, policy version and services come from site.consent, not markup props.
tone
RefOrValue<SemanticTone> - Default
neutral
Semantic intent: neutral, accent, secondary, info, success, warning, danger, feature, custom or ghost. The active skin supplies the colors.
variant
RefOrValue<ComponentVariant> - Default
surfaceAlt
Visual treatment. Accepts solid, surface, surfaceAlt, spotlight, glass, flat, flatAlt, flatSolid, outlineFill, outline, subtle, subtleBtn, link, sheen, underline, rail, bracket or none.
variantMode
RefOrValue<ComponentVariantMode> - Default
stateless
Use stateless for content surfaces; stateful enables the treatment’s hover, focus and active styles. It does not add interaction handlers.
Composition
No public slots. The settings button teleports to .consent-settings-teleport-area, provided by SiteFooter or by your own template. Register defineConsentComponents with Panel, Flex, Btn, BtnLink and FormCheck. Include the standard consent runtime with the same resolved configuration.
Configuration responsibilities
enabledcontrols whether the component exists.storageKeyisolates saved choices;policyVersionidentifies the current policy.categoriesdefine IDs, visible labels, descriptions and required state.servicesdescribe the services controlled by those categories.- Banner, button and policy-link copy belong in the consent configuration.
The preview source shows the complete scoped override used by the working example.