ui-038

Cookie Consent Banner

£7 or the whole Vault, £79.20

A GDPR cookie consent banner: a compact card Flip-morphs into per-category preferences, fires a consent event and remembers the choice.

Flip intermediate
6 more details
flip-layoutstate-transitionmicro-interactionstaggerkeyboard-navigationfocus-management

This element reads better at your own screen size than in the frame below:

Updated

About this component

A cookie consent banner built as a consent manager rather than a notice. The compact card rises from the bottom edge on a settled power4 ease with its icon, title, copy and buttons staggering in. Pressing Manage preferences morphs that card into a taller preferences panel with GSAP Flip, tweening its width, height and position so the type never distorts, while the category rows stagger in from below. Each category is a real switch with a springy knob and a cross-fading track; Necessary is locked on with a padlock and a disabled control.

Read the full effect overview

Accept all, Reject non-essential and Save preferences all resolve to one object of category booleans, store it in localStorage under your key with a version, and dispatch a consent:change event on the root so you can gate analytics, marketing and personalisation scripts on it. Accept and Save turn the pressed button into a drawn check before the card folds toward its bottom edge. On later visits a valid stored choice keeps the banner hidden; bump the version to re-ask everyone.

Three positions on one attribute: a bottom-left card, a full-width bottom bar and a centred modal with a backdrop, focus trap and scroll lock. Escape only ever steps back from the preferences panel, because dismissing a consent banner must never count as consent. On phones every position becomes a bottom sheet with stacked 48px buttons and an internally scrolling list. Two themes, light and dark, defined entirely in CSS custom properties on one short list of neutral product tokens: dark is the same design re-valued, not a second skin.

Cookie Consent Banner - UI component preview

What's included

12 items
  • Compact card rises on power4.out with the icon, title, copy and buttons staggering in
  • GSAP Flip morph from the card to the preferences panel and back, tweening width and height so text is never squashed
  • Per-category role=switch controls with a back.out knob spring and a cross-fading track; a locked Necessary category with a padlock
  • consent:change event with one boolean per category plus the source (accept-all, reject or save)
  • Choice stored in localStorage under data-storage-key with a data-version, so a version bump re-asks everyone
  • Accept and Save morph the pressed button into a drawn check, then the card folds toward its bottom edge on expo.in
  • Three positions from one attribute: bottom-left card, full-width bottom bar, centred modal
  • The centred position is a real dialog: backdrop, focus trap, scroll lock, focus return; the edge positions never steal focus
  • Escape closes the preferences panel only, never the banner, so a dismissal is never silent consent
  • Public API: show, hide, openPreferences, closePreferences, acceptAll, rejectAll, save, get, reset
  • Phone bottom sheet under 640px with stacked 48px buttons, 44px switch targets and an internally scrolling list
  • Two themes (light and dark) as CSS custom property blocks on one neutral token scale; reduced motion mounts every state instantly

Perfect for

5 use cases
  • GDPR and ePrivacy cookie consent on marketing sites and web apps
  • Gating analytics, advertising and personalisation scripts on a recorded choice
  • A cookie settings link in the footer that reopens the preferences panel
  • Re-asking every visitor after a policy change by bumping one version attribute
  • Product sites that want the consent flow to match the rest of the interface instead of a third-party widget

How it works

3 sections

The rise and the morph

The card starts below the fold under GSAP's own control, never a CSS transform, and comes up on an ease that lands with no overshoot. Pressing Manage fades the compact copy out, records the card with Flip, swaps the hidden attributes, then animates the difference in position, width and height. Scale is deliberately off: scaling a box with text squashes the type for the whole tween, while tweening width and height keeps it upright. The rows stagger in while the card is still growing, and the inline size is cleared on completion so the card hands back to its CSS.

Springy switches, settled card

Each knob's travel is taken from its track at the moment it toggles, so a variant can resize the switch in CSS alone. The knob moves with a spring and the track and knob colours cross-fade underneath on a plain ease, read from the switch's custom properties at call time. The springiness lives only there: the card itself never bounces.

Deciding, storing, folding

Accept, Reject and Save resolve to one object of category booleans, write it to localStorage with the version, and dispatch consent:change before anything animates, so a listener runs even if the page navigates mid-fold. Accept and Save then draw a check into the pressed button; all three pin the card's height and tween it, with its padding, to zero on expo.in. The hidden attribute is set in onComplete so no invisible layer is left covering the page, and in the centred position the scroll lock releases and focus returns to where it was.

Plugins Flip
Difficulty Intermediate
Includes HTML + JS + CSS source, documentation, AI setup prompt, lifetime updates

Lighthouse, as measured

Google Lighthouse on this UI element's demo, from the latest scan. A measurement of the demo as shipped, not a promise for your page.

Accessibility
100
Best practices
100

No performance score, on purpose. That figure depends on how you deploy: your server's compression and caching, your CDN, the connection and the device doing the test, none of which the code controls. The same page can score very differently on two consecutive runs, so measure it where it will live.

Paid UI element

Purchase to unlock the code.

Buying Cookie Consent Banner opens the HTML, CSS and JavaScript source, the full documentation, an AI setup prompt for your editor, and every update we ship to it. Standard license: unlimited personal and commercial projects. Every paid product is tested for desktop and mobile layouts, reduced-motion handling, animation cleanup and keyboard controls wherever there is something to operate.

£7 Standard license, unlimited projects

Browse UI elements

Or the whole library: the Vault, £79.20, one payment.

Documentation

Quick Start

1. Add to your HTML <head>:

Code snippet omitted: it ships with the download.

2. Add before the closing </body> tag:

Code snippet omitted: it ships with the download.

3. Copy the component markup (the block between <!-- Component starts here --> and <!-- Component ends here --> in index.html) into your page, anywhere in <body>. It is one root element:

Code snippet omitted: it ships with the download.

4. Gate your scripts on the event:

Code snippet omitted: it ships with the download.

Using It With Your Own Design

What the script needs from your markup. The root carries data-cookie-consent; inside it, data-card is the box that rises and morphs, data-head holds the icon and title (it stays through both states), data-compact is the short version and data-preferences is the panel. The buttons are found by attribute, not by class or text: data-accept, data-reject, data-manage, data-save and data-back. Each category is an <li data-category="name"> in the list containing a <button role="switch"> with a data-track span and a data-knob span inside it. Add data-locked to a category that can never be switched off; the script disables its switch and reports it as true in every payload. Anything marked data-reveal staggers in with the intro; leave the attribute off an element and it simply appears.

Categories are yours. Add or remove <li data-category="..."> rows and the event payload, the stored choice, acceptAll() and rejectAll() all follow. The category name in the attribute is the key you will read in event.detail.

What is only the demo's furniture. The short cookie-policy document behind the banner (.stage-doc), the 44px toolbar strip across the top with the position, reset and theme controls, and the Google Fonts link. None of it is part of what you bought.

CSS the component depends on. Do not put a CSS transform on .cc-card or .cc-knob: GSAP owns both and reads a CSS translate as a pixel offset it never returns from. Do not give .cc-card a min-height, or the Flip morph cannot shrink it back. Keep overflow: hidden on the card so the fold clips its content. The data-lenis-prevent on the list is for pages that use Lenis smooth scroll; it is the one vertically scrolling panel in the component, and it must not be moved onto anything else.

The policy is yours too. The component provides the mechanism: the categories, the switches, the event, the stored choice. The text in the banner, which cookies belong in which category, and which scripts you hold back until the event fires are decisions for you and whoever advises you on the regulation you operate under.

What's Included

  • index.html: the demo page, the component markup, both themes and three positions

  • assets/style.css: the component, its two themes and the phone bottom-sheet layout

  • assets/script.js: readable, commented source with the public API, the consent event and cleanup

  • assets/script.min.js: minified production script

  • thumbnail.html: build-only capture state

  • A compact card that rises on power4.out with its icon, title, text and buttons staggering in

  • One GSAP Flip morph from the compact card to the preferences panel and back, tweening width and height so the type never distorts

  • Per-category switches with a springy knob (back.out) and a cross-fading track; a locked category with a padlock and a disabled switch

  • The pressed Accept or Save button turns into a drawn check, then the card folds toward its bottom edge on expo.in

  • Three positions on one attribute: bottom-left, bottom-bar and center; only center is a modal

  • The choice is stored under your key with a version, so bumping the version re-asks everyone

  • Roles: region (or dialog with aria-modal in center), switch with aria-checked, aria-expanded and aria-controls on the Manage button

Positions

Code snippet omitted: it ships with the download.

Value What you get
bottom-left (default) A card in the bottom-left corner; the preferences panel grows upward from it
bottom-bar A full-width strip along the bottom edge with the buttons in a row; the panel grows upward
center A centred card over a dimmed backdrop. This position is a modal: focus is trapped, the page behind is locked and focus returns on close

On screens 640px and narrower every position becomes a full-width bottom sheet: the buttons stack at 48px tall and the list scrolls inside a 70vh cap.

Themes

Two themes, light (the default) and dark: one design at two sets of token values, not two designs. Pick one by setting the attribute on <body> or on the banner root:

Code snippet omitted: it ships with the download.

Nothing else changes: same markup, same script, and nothing in assets/script.js ever reads the theme name. The demo's toggle and the ?variant=dark URL parameter only set that attribute.

Both blocks live at the top of assets/style.css, and this is the whole list to re-value for your own brand:

Token What it colours
--ground / --ground-2 the page behind the banner, and the demo toolbar strip
--raised / --hover the card itself, and the hover state of a button
--ink / --ink-2 / --ink-3 the title, body copy, and the category descriptions
--line / --line-strong the card's hairline, the row rules, and a button's edge
--accent the focus ring, ::selection, and the drawn check
--accent-strong / --accent-ink the primary button's fill and its label
--shadow / --backdrop the card's elevation, and the dim behind the centred position
--cc-switch-on / --cc-switch-off / --cc-knob-on / --cc-knob-off the switch track and knob in each state
--radius / --radius-sm / --radius-xs card, control and tag corners

The four switch colours are read by the script with getComputedStyle at the moment it tweens them, so give them literal colours, never a var() reference to another token. --accent-strong exists because a saturated red cannot carry white label text at 4.5:1; it is the same red deepened just enough to clear it.

To add a third theme, copy either [data-variant="..."] block, rename the value and change the properties. A new theme never needs a selector override. Both use one face, Inter, loaded in the demo from Google Fonts; point --font at whatever your page already loads and drop the link.

Keyboard & Accessibility

Key What it does
Tab / Shift+Tab Moves through every control in DOM order. In center it cycles inside the dialog
Enter / Space Presses the focused button; on a switch, toggles it
Escape Closes the preferences panel back to the compact card, with focus returned to Manage preferences. It never dismisses the banner: a dismissal would have to count as consent or refusal, and it is neither
  • The root is role="region" with aria-label="Cookie consent" in the two edge positions, and role="dialog" with aria-modal="true" and aria-labelledby pointing at the title in center. The script sets these itself from data-position, so a change of position never leaves the wrong role behind.
  • Each category control is a real <button role="switch" aria-checked>; the locked one is disabled and reads as "always on".
  • The Manage button carries aria-expanded and aria-controls for the panel.
  • In center, focus moves to the first button on show and returns to whatever was focused before on hide; body scrolling is locked while it is open. In the other positions focus is never taken on page load.
  • Opening the panel moves focus to its first switch; closing it puts focus back on the Manage button.
  • prefers-reduced-motion: the banner mounts in its final state, the Flip morph becomes an instant swap, the switches cut, and the fold is an instant hide. Every end state is identical.
  • Without JavaScript the compact card is visible and complete; only the interaction is missing.

How It Works

The rise. The card starts at yPercent: 110 from a gsap.set, never a CSS transform, and comes up on power4.out, which lands with no overshoot: a consent banner that bounces reads as a marketing popup. Its icon, title, text and buttons follow on a short stagger. The centred card has no edge to rise from, so it lifts a short way and fades instead.

The morph. Pressing Manage fades the compact text and buttons out, records the card with Flip.getState(), swaps the hidden attributes and a class, then Flip.from() animates the difference in position, width and height. scale is left off on purpose: scaling a box with text squashes the type for the whole tween, while tweening width and height keeps it upright. The category rows stagger in from below while the card is still growing, and the inline width and height are cleared on completion so the card hands back to its CSS.

The switches. Each knob's travel is measured from the track at the moment it is toggled, so a theme can resize the track in CSS alone. The knob moves on back.out(2.2), overshooting its resting point and settling, while the track and knob colours cross-fade underneath on a plain ease; the colours are read from the switch's own custom properties at call time, so no tween hard-codes one. The track is ink, not the accent: the accent has one job here, the primary button.

Deciding. Accept, Reject and Save all resolve to one object of category booleans, write it to localStorage with the version, and dispatch consent:change on the root before anything animates, so your listener runs even if the page navigates away mid-fold. Accept and Save then fade the pressed button's label out and draw a check in its place; all three fold the card by pinning its height and tweening it, with its padding, to zero on expo.in. hidden is set in the fold's onComplete, so an invisible layer is never left covering the page.

Remembering. On load the script reads the stored choice; if one exists with a matching version the banner stays hidden and the switches are set to it, ready for openPreferences() from a footer link. reset() clears it and shows the banner again.

Customisation

  • --cc-width / --cc-width-prefs (and the -center pair) set the card's width in each state; the morph tweens between them; --cc-pad and --cc-gap set its padding and its distance from the viewport edge.
  • Colours, fonts, radii and shadow are custom properties on each [data-variant] block in assets/style.css (the table under Themes is the full list).
  • The rise, the morph and the fold are the gsap.to, Flip.from and gsap.timeline calls in assets/script.js; the knob spring is back.out(2.2) in paintSwitch.
  • Set data-storage-key to namespace the stored choice and data-version to re-ask everyone after you change your policy.
  • Set data-label on the root to change the region's accessible name.

Requirements

  • GSAP 3.12+ plus Flip (the demo uses 3.15.0)
  • No build step, no framework

Worked examples, the events and programmatic API, and the class reference ship with the download, alongside the full source.