Pular para o conteúdo

Cookie-consent component

components specs/components/cookie-consent.kmd

The tracking-consent surface: a first-tier bar offering accept/reject with equal prominence, and a second tier for per-category granularity. Its semantics — the record shape, the categories, opt-in default-deny, re-show conditions, the gating API — are NOT defined here; they live in `privacy/consent-record.kmd`, and this spec is the component layer over them (anatomy, tiers, geometry per active DS, focus, i18n, and the test that proves the choice is enforced). Shipped for web as `<koder-cookie-banner>` in `koder_web_kit`; gov.br ships `.br-cookiebar` (LGPD).

Quando esta spec se aplica

Triggers primários

Todos os triggers

Corpo da especificação

Component — Cookie consent

Status: v0.1.0 — Draft. Opened from the KDS × gov.br gap analysis (tools/design-gen/docs/govbr-ds-gap-analysis.md, stack#394). Anatomy below is MEASURED from @govbr-ds/core v3.7.0 dist/core.min.css (.br-cookiebar, 60 occurrences), and cross-read against the shipped Koder reference implementation (koder_web_kit <koder-cookie-banner> + its Playwright test), so it describes what exists rather than what a cookie bar is imagined to be.

The gap this spec closes is narrower than it looks

The gap analysis recorded "gov.br tem cookiebar; KDS não tem". That is false for web and was already false when it was written: privacy/consent-record.kmd (2026-06-02) defines the contract, and koder_web_kit ships KoderConsent + <koder-cookie-banner> — two-tier, localized, tested, and deployed on the Koder sites (meta/sites/areas/*/_kit/). What was actually missing is this file: a component contract, so a non-web surface has something to implement against and a reviewer has something to judge against.

Everything about consent as data therefore stays in consent-record.kmd and is not restated here (reuse-first.kmd): the record shape, the category set, opt-in/default-deny (its R1), reject-as-easy-as-accept (its R2), the re-show conditions (R3), the optional audit POST (R4), the allows() gating API (R5). Duplicating those would create two sources of truth for a legal obligation, which is the one place drift is least affordable.

R1 — The gate is the component; the banner is its surface

A consent UI whose answer nothing consults MUST NOT ship. This is the rule the rest of the component hangs on.

A banner that renders, records a choice, and reports success while collection proceeds regardless is not a partial implementation — it is worse than shipping no banner at all. Absent a banner, a surface is visibly non-compliant and someone fixes it. With a decorative one, the surface manufactures evidence that the user was asked and answered, and both the user and the operator believe a choice is being honoured that is not. It fails LGPD Art. 8 (consent must be informed and specific) precisely by looking like it satisfies it.

So a surface claims this component only when a real collector reads consent-record.kmd R5's gate before collecting. A surface with no gated collector does not need this component and MUST NOT render it — asking for consent that governs nothing is its own dark pattern.

Consequence, stated so nobody has to infer it: this component is not implementable on a surface that has no consent store and no gated collector, and the honest response there is to build the gate first (see Native surfaces below).

R2 — Two tiers, one component

Measured: gov.br models both tiers in a single component, switched by one class.

Tiergov.brAnatomy
First (.br-cookiebar.default)top: unset → pinned to bottom: 0, full width.info-text shown; .entry-content, .main-content, .complementary-content hidden. Actions: accept + reject + manage.
Second (.br-cookiebar, no .default)position: fixed, top/right/bottom/left: 0 — the whole viewportNests .br-modal: header + title + close, body listing the categories, footer actions.

Both sit at z-index: --z-index-layer-4 under --surface-shadow-lg-up. The second tier is a modal (dialogs.kmd): it traps focus and is dismissible; the first tier is not a modal — it must never trap focus, because a bar the user has not answered must not hold the page hostage.

The first tier's three actions carry equal weight by consent-record.kmd R2; that rule is normative there, and this spec's only addition is where they sit: all three in the same action row, no scroll, no disclosure.

R3 — Categories render as switches, necessary as inert

Per-category controls are switch.kmd (immediate effect, no submit), one per category from consent-record.kmd's set. The reserved necessary category renders visibly present and non-interactive (gov.br: .always-active) — never hidden, never a switch the user can move and watch snap back. It is stated, not offered; a control that silently refuses input teaches the user their input is being ignored, which is exactly what the rest of the dialog is trying to disprove.

R4 — Geometry + colour per active design system

Chrome geometry comes from the active DS (theme::Shape, kroma#278) and tones from its Palette (kroma#194), per design-RFC-014 — not from constants copied out of gov.br's CSS. A gov.br surface then gets gov.br's own radii (square) and a Verge surface gets Verge's, from the same component.

R5 — Accessibility

  • The first tier is a banners.kmd region, announced politely — it is not an alert and must not interrupt a screen reader mid-sentence.
  • The second tier follows dialogs.kmd: focus moves in on open, is trapped while open, and returns to the invoking control on close.
  • Every action is a real, translated sentence, en-US + pt-BR floor (i18n/contract.kmd) — the shipped web banner is already en/pt/es.
  • Reject must be reachable by keyboard in the same number of stops as accept. A reject that costs more keystrokes than accept fails consent-record.kmd R2 for keyboard users specifically, which is the group least likely to be measured.

R6 — Test

A surface claiming this component MUST prove, headlessly:

  1. Collection is gated — the collector does not collect before a choice exists, and does collect only the categories opted into. This is the negative control for R1: a decorative banner passes every other assertion here and fails only this one. Assert it against the collector, never against the banner's own state — a banner agreeing with itself proves nothing.
  2. necessary is allowed with no record; every other category is denied (this is consent-record.kmd R1 observed through the component).
  3. Reject and accept are both reachable, in the same number of keyboard stops.
  4. A recorded choice survives a reload and the first tier does not re-appear — and it does re-appear on each R3 re-show condition.
  5. The second tier returns focus to its invoker on close.

The shipped web reference (koder_web_kit/test/cookie-banner.test.mjs) covers 2 and 4 today against a real browser. (1) is the one to add: it is the only assertion that can tell a working consent surface from a decorative one, and no current test makes it.

Native surfaces (Kroma) — what must exist first

koder-app/behaviors.kmd §2 mandates telemetry from every Koder app and says nothing about consent, and no native SDK has a KoderConsent counterpart — so a native app today collects with no gate to consult. A Kroma banner built now would therefore be exactly the R1 failure this spec forbids: a surface that records a choice nothing enforces.

The order is fixed by R1, not by convenience: a native consent store + a telemetry client that gates on it, and only then the component. Tracked as kroma#280.

  • specs/privacy/consent-record.kmdthe semantics: record, categories, R1–R5.
  • specs/components/banners.kmd — the first tier's region + re-show discipline.
  • specs/components/dialogs.kmd — the second tier's focus contract.
  • specs/components/switch.kmd — the per-category control.
  • policies/identity-data-retention.kmd — retention of any server-side audit copy.

Requirements (testable)

A surface claiming this component SHALL gate a real collector on the consent record before collecting, SHALL collect only the categories opted into, and SHALL NOT render the banner on any surface that has no consent store and no gated collector.

Scenario: No collection before a choice exists

  • GIVEN a surface with the cookie-consent banner rendered and no consent record yet stored
  • WHEN the page loads and the banner is displayed but no accept/reject choice has been made
  • THEN the gated collector performs no collection for any non-necessary category
  • AND the collector records zero collection events even though the banner is present on the page

Scenario: Only opted-in categories are collected

  • GIVEN a visitor who has accepted the analytics category and rejected marketing
  • WHEN collection runs after the choice is recorded
  • THEN the collector collects data for analytics
  • AND the collector collects no data for marketing

Scenario: Banner is absent where nothing is gated

  • GIVEN a surface with no consent store and no gated collector
  • WHEN the surface renders
  • THEN the cookie-consent component is not rendered

The component SHALL present a first tier that is a non-modal banner region carrying accept, reject, and manage actions in one action row, and a second tier that is a modal dialog listing per-category controls; the first tier SHALL NOT trap focus and the second tier SHALL trap focus while open.

Scenario: First tier does not trap focus

  • GIVEN the first-tier consent bar is shown and the user has not answered
  • WHEN the user presses Tab past the bar's actions
  • THEN focus moves to page content behind the bar
  • AND focus is not held within the bar

Scenario: First tier exposes all three actions in one row

  • GIVEN the first-tier consent bar is shown
  • WHEN its action row is inspected
  • THEN accept, reject, and manage actions are all present in the same action row
  • AND no disclosure or scroll is required to reach any of them

Scenario: Second tier traps focus while open

  • GIVEN the second-tier preferences dialog is open
  • WHEN the user tabs through the last focusable control in the dialog
  • THEN focus wraps to a control inside the dialog
  • AND focus does not leave the dialog while it is open

The second tier SHALL render one switch per consent category with immediate effect and no submit, and SHALL render the reserved necessary category visibly present and non-interactive rather than as a movable switch or a hidden control.

Scenario: A category switch takes effect immediately

  • GIVEN the preferences dialog is open with the analytics category off
  • WHEN the user toggles the analytics switch on
  • THEN the analytics consent takes effect without any submit action
  • AND the recorded consent for analytics reads allowed

Scenario: Necessary category is present but not interactive

  • GIVEN the preferences dialog is open
  • WHEN the necessary category control is inspected
  • THEN it is visibly present in the category list
  • AND it cannot be toggled off by the user

The component SHALL announce the first tier politely (not as an interrupting alert), SHALL make reject reachable by keyboard in the same number of stops as accept, and SHALL return focus to the invoking control when the second tier closes.

Scenario: Reject costs no more keyboard stops than accept

  • GIVEN the first-tier consent bar is shown and focus enters the bar
  • WHEN the user tabs to the accept action and, separately, to the reject action
  • THEN the number of keyboard stops to reach reject equals the number to reach accept

Scenario: First tier is announced politely

  • GIVEN a screen reader is active when the first-tier bar renders
  • WHEN the bar appears
  • THEN it is announced via a polite live region
  • AND it does not interrupt the screen reader mid-sentence as an alert would

Scenario: Second tier returns focus to its invoker

  • GIVEN the second-tier preferences dialog was opened from the manage action
  • WHEN the user closes the dialog
  • THEN focus returns to the manage control that invoked it

A recorded choice SHALL survive a page reload without re-showing the first tier, and the first tier SHALL re-appear on each re-show condition defined by the consent record.

Scenario: Choice survives reload without re-prompting

  • GIVEN a visitor who has recorded a consent choice
  • WHEN the page is reloaded
  • THEN the stored choice is still in effect
  • AND the first-tier bar does not re-appear

Scenario: First tier re-appears on a re-show condition

  • GIVEN a stored consent choice that has become stale per a consent-record re-show condition
  • WHEN the surface loads
  • THEN the first-tier bar re-appears to request consent again

Referências