Cookie Consent Block
A consent banner and a preferences dialog, written by one block. It is the worked example for revealing the place a FIELD is edited — data-block-selector="uid#fieldName".
Live example
Developer Reference
Schema
Pass this object inside the blocks option when calling initBridge() to register this block type with the admin UI. See Custom Blocks for the full setup guide.
{
"fieldsets": [
{
"id": "default",
"title": "Default",
"fields": [
"message",
"analyticsPurpose"
]
}
],
"properties": {
"message": {
"title": "Banner message",
"type": "array",
"widget": "slate",
"description": "Shown in the consent banner, at the foot of every page, until a visitor chooses."
},
"analyticsPurpose": {
"title": "Analytics cookies — what they are for",
"type": "string",
"widget": "textarea",
"description": "Shown beside the analytics tick box, inside the preferences dialog."
}
},
"required": []
}JSON Block Data
Example JSON as stored in the Plone content API. This is the data structure your component will receive in the block prop.
{
"@type": "cookieConsent",
"analyticsPurpose": "Counts visits and pages, so we can see what is worth improving. Never used to identify you.",
"message": [
{
"type": "p",
"children": [
{
"text": "We use essential cookies to make this site work, and analytics cookies to see how it is used."
}
]
}
]
}Rendering
How this block renders in your frontend. Add its handling to your renderer, or — for list-style blocks — register a fetcher and reuse your list rendering.
// A block drawn in three places, two of them hidden.
//
// Cookie consent is the everyday case for `data-block-selector="uid#field"`.
// The block's own element is a bar; its `message` is read in a BANNER at the
// foot of the page, and its `analyticsPurpose` beside a tick box inside a
// PREFERENCES DIALOG. Both sit OUTSIDE the block's element — a real frontend
// usually portals them to <body>, which is where a design system's own
// JavaScript puts them — and both are hidden until their trigger is pressed.
//
// So "is the block visible?" is the wrong question: the bar is always visible,
// and neither half of what an author writes is. Each trigger therefore names
// the FIELD its half holds, and the bridge opens the half whose field the
// author reached for in the sidebar.
function CookieConsentBlock({ block }) {
const uid = block['@uid'];
const [showBanner, setShowBanner] = useState(true);
const [showDialog, setShowDialog] = useState(false);
return (
<>
{/* The block's own element: always on screen, and the way back to two
halves a visitor may already have dismissed. */}
<div data-block-uid={uid} className="cookie-consent__bar">
<strong>Cookie consent</strong>
<button
type="button"
// "I reveal where `message` is edited." Put the cursor in Banner
// message in the sidebar and the bridge clicks this, so the banner is
// on screen while its wording is written.
data-block-selector={`${uid}#message`}
onClick={() => setShowBanner(true)}
>
Show the banner
</button>
<button
type="button"
// The other half. One handle could not serve both: it is one click,
// and whichever it opened, the other half's wording would stay
// unreachable from the sidebar.
data-block-selector={`${uid}#analyticsPurpose`}
onClick={() => setShowDialog(true)}
>
Show cookie preferences
</button>
</div>
{/* Outside the block's element, and annotated where the text is READ.
Each half advertises the field it holds, as well as the bar's trigger doing so.
Without that the wording inside belongs to no block — it sits outside the block's
element, so `data-edit-text` there resolves to nothing and cannot be edited. The
trigger in the bar is still what the bridge CLICKS: a hidden handle cannot open
anything, so the bridge takes the first one that is on screen. */}
<div
className="cookie-banner"
data-block-selector={`${uid}#message`}
hidden={!showBanner}
role="alert"
>
<p data-edit-text="message">{slateToText(block.message)}</p>
<button type="button" onClick={() => setShowBanner(false)}>
Accept all
</button>
<button type="button" onClick={() => setShowDialog(true)}>
Manage preferences
</button>
</div>
<div
className="cookie-dialog"
data-block-selector={`${uid}#analyticsPurpose`}
hidden={!showDialog}
role="dialog"
>
<h2>Manage cookie preferences</h2>
<label>
<input type="checkbox" name="analytics" />
Analytics
</label>
<p data-edit-text="analyticsPurpose">{block.analyticsPurpose}</p>
<button type="button" onClick={() => setShowDialog(false)}>
Save
</button>
</div>
</>
);
}
// Slate is an array of nodes; the banner shows its text. A real frontend would
// render the marks and links too — kept flat here so the pattern stays visible.
function slateToText(value) {
if (!Array.isArray(value)) return '';
return value
.map(node => (node.text ?? (node.children || []).map(c => c.text ?? '').join('')))
.join('');
}