Consent Group

Consent checkboxes that are never pre-ticked and keep required terms apart from optional purposes, and record what was agreed and when.

StopsPre-ticked consent bundled with terms

Installation

pnpm dlx shadcn@latest add https://bollard-ui.vercel.app/r/consent-group.json

The files land in your project. They're yours to edit.

Usage

consent-group-usage.tsx
import { ConsentGroup, type ConsentChoices, type ConsentItem, type ConsentRecord } from "@/components/ui/consent-group"

<ConsentGroup
  items={items}
  version="2026-10"
  value={choices}
  name="consent"
  onValueChange={(next, entry) => {
    setChoices(next)
    setRecord(entry)
  }}
/>

What it catches

  • Pre-ticked consent boxes
  • Optional purposes bundled with required terms
  • "Accept all" ticking more than the person read, by listing it first
  • Changes to consent wording, via a version and a fingerprint in each record

Props

ConsentGroup props
PropTypeDefault
items* (required)

The consent checkboxes, required and optional.

ConsentItem[]—
version* (required)

Version of this consent text, stored in every record.

string—
value

Controlled choices, e.g. a person's saved record when they edit it. There is deliberately no `defaultValue`: uncontrolled, every box starts unticked.

ConsentChoices—
onValueChange

Called with the new choices and the record to store.

(choices: ConsentChoices, record: ConsentRecord) => void—
name

Adds a hidden input carrying the JSON record, for plain form posts.

string—
legend

Visible title for the group.

string"Your choices"
requiredHeading

Heading for the required items.

string"Needed for your account"
optionalHeading

Heading for the optional purposes.

string"Optional, your choice"
acceptAllLabel

Text for the accept-all button.

string"Accept all"
declineLabel

Text for the decline-optional button.

string"Decline optional"
acceptAllText

`{list}` is what Accept all would tick.

string"This also ticks {list}."
confirmLabel

Text for confirming accept-all.

string"Tick them"
cancelLabel

Text for cancelling accept-all.

string"Cancel"
recordedText

`{when}` and `{version}` are filled in.

string"Recorded {when} · text {version}"

* required. Also accepts the native props of <fieldset> (className, aria-*, data-*…), passed through to it.

Accessibility

  • Required and optional sections are role="group" with headings via aria-labelledby
  • Each checkbox has a <label> and aria-describedby to its description
  • Accept-all confirmation is a labelled group; focus moves to Confirm and Escape cancels
  • The required marker is aria-hidden; requirement is stated in the heading

Only what the source does. Test with your own screen reader and content before shipping.

Limitations

This is a UX guard rail. Enforce on the server: Store consent records server-side with the exact text version shown, and let users withdraw as easily as they gave consent.

  • The fingerprint is a short FNV-1a hash for spotting wording changes, not a tamper-proof signature
  • Records are passed to you; storing them and honouring withdrawal is up to your backend
  • Not legal advice: whether consent is valid depends on your jurisdiction and context

Rules

Rules not yet verified against an official source.

Not verified against the primary source on 2026-10-17: EUR-Lex could not be fetched. Recital 32 text ("Silence, pre-ticked boxes or inactivity should not therefore constitute consent") was read on a secondary reproduction only. Informational, not legal advice. Copied components don't update themselves, so re-check after rule changes (see updating a component).

What it stops

Pre-ticked consent bundled with terms

Also known as

consent · checkbox · gdpr · dpdp · marketing emails · newsletter opt-in · cookie consent · terms checkbox

Dependencies

Pairs well with