GODRICH

Cookie Consent Banner UI — 9 Patterns to Paste In

A cookie consent banner ui is the strip or card that tells visitors a site stores cookies and records their choice.

Auto-plays · click a tile to jump to its section · all nine in one zip

The order follows the sequence in which a visitor meets consent, not how often each shape shows up in the wild. It opens with the strip that rises from the bottom edge, moves to the panel that splits consent into separate switches and then to the button that reopens a choice already made, and shrinks the footprint into a corner card and a single-line bar. After that come the pair that gives accept and reject the same weight, the small motion that marks the moment of agreement, the gate that waits for the end of the text, and finally the confirmation that the setting was stored. Every one of them writes its result to localStorage, so the value is still there after a reload.

Number Where it sits Animated property
01 · 05 Strip across the bottom edge translateY
02 Three rows inside a card grid-template-rows · translateX
03 Stage center, plus a bottom-left button scale · opacity
04 Bottom-right corner translateY · rotate · scale
06 Two buttons inside a card opacity · scale
07 A centered icon and three crumbs translate · scale
08 Progress bar above a scroll box scaleX
09 Toast at the top right translateX · scaleX

01Bottom bar slide-up

The strip waits below the mock page by its own height plus $sp-2, rides up into place, and leaves along the same line once a button is pressed. The label of whichever button you pressed then stays on the page as a small chip.

translateY 100%→0$dur-enterradius 16
.cb.is-demo .cb__banner { animation: cb-slide $dur-loop $easing infinite; }

@keyframes cb-slide {
  0%        { transform: translateY(calc(100% + #{$sp-2})); opacity: 0; }
  22%, 72%  { transform: translateY(0); opacity: 1; }
  100%      { transform: translateY(calc(100% + #{$sp-2})); opacity: 0; }
}

02Category toggle panel

Necessary, analytics, and marketing each own a switch, the necessary one locked on, and pressing a row title grows its description from 0fr to 1fr. A height of auto cannot be transitioned, so the grid row size carries the animation instead.

grid 0fr→1fr노브 translateXradius 16
.ct__panel {
  grid-column: 1 / -1;
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows $duration $easing;
}
.ct__inner { overflow: hidden; }
.ct__row.is-open .ct__panel { grid-template-rows: 1fr; }

.ct__knob { transform: translateX(0); transition: transform $duration $easing; }
.ct__sw.is-on .ct__knob { transform: translateX(16px); }

03Manage-consent modal reopen

A round button in the bottom-left corner breathes once, and pressing it drops a backdrop over the page while the modal grows from .92 to 1. Focus moves to the first button inside on open and returns to the round button on close.

scale .92→1백드롭 opacityradius 16
.mo__modal {
  width: min(100%, 264px);
  opacity: 0;
  transform: scale(.92);
  pointer-events: none;
  transition: transform $duration $easing, opacity $duration $ease-out;
}
.mo.is-open .mo__modal { opacity: 1; transform: scale(1); pointer-events: auto; }

.mo__backdrop { background: rgba(23, 20, 26, .44); opacity: 0; pointer-events: none; }
.mo.is-open .mo__backdrop { opacity: 1; pointer-events: auto; }

04First-visit corner card

The card arrives tilted four degrees and straightens itself while three purpose rows follow at 0.10, 0.16, and 0.22 seconds. Reusing one keyframe and changing only the delay saves writing a separate animation per row.

rotate 4deg→00.1s 시차radius 16
@keyframes fv-card {
  0%, 100%  { opacity: 0; transform: translateY(16px) rotate(-4deg) scale(.96); }
  18%, 78%  { opacity: 1; transform: translateY(0) rotate(0deg) scale(1); }
}

.fv__card.is-demo { animation: fv-card $duration $easing infinite; }
.fv__card.is-demo .fv__row { animation: fv-row $duration $easing infinite; }
.fv__card.is-demo .fv__row--a { animation-delay: .10s; }
.fv__card.is-demo .fv__row--b { animation-delay: .16s; }
.fv__card.is-demo .fv__row--c { animation-delay: .22s; }

05One-line mini bar

A 44px strip slides up and takes almost nothing away from the page, and inside it only the cookie icon rocks from side to side. The tilt runs -6deg → 6deg → -6deg, so the end of a cycle already matches its start.

높이 44px 고정rotate ±6degradius 12
@keyframes mb-up {
  0%, 100%  { transform: translateY(100%); }
  20%, 82%  { transform: translateY(0); }
}
@keyframes mb-tilt {
  0%   { transform: rotate(-6deg); }
  50%  { transform: rotate(6deg); }
  100% { transform: rotate(-6deg); }
}

.mb__bar.is-demo { animation: mb-up $duration $easing infinite; }
.mb__bar.is-demo .mb__cookie { animation: mb-tilt $duration $ease-out infinite; }

06Equal-weight accept and reject

Both buttons carry the same 2px border and the same font weight, and only accept fills its background. Each button carries its own focus ring in an ::after, and the two alternate by opacity and scale, so a difference in width can never park the ring on the wrong label.

outline 2pxscale 0.97 누름radius 12
.ep__pair { display: grid; grid-template-columns: 1fr 1fr; gap: $sp-2; }
.ep__btn {
  border: 2px solid $color;
  background: $stage-paper;
  color: $color;
  font-weight: 700;
}
.ep__btn--accept { background: $color; color: $stage-paper; }
.ep__btn::after {
  content: "";
  position: absolute;
  inset: -6px;
  border: 3px solid $color;
  border-radius: $r-control;
  opacity: 0;
}

07Crumbling cookie icon

The cookie dips and swells while three crumbs travel out at separate angles and come home to the same spot. The looping version and the one-shot version share the same keyframes, and only the iteration count is overridden afterward.

scale .88→1.08부스러기 3개 왕복radius 50%
@keyframes ck-crumb-a {
  0%   { transform: translate(0, 0) scale(.4);        opacity: 0; }
  16%  { transform: translate(-12px, -10px) scale(1); opacity: 1; }
  46%  { transform: translate(-34px, -26px) scale(1); opacity: 1; }
  78%  { transform: translate(-10px, -8px) scale(.7); opacity: .5; }
  100% { transform: translate(0, 0) scale(.4);        opacity: 0; }
}

.ck.is-demo .ck__crumb--a,
.ck.is-burst .ck__crumb--a { animation: ck-crumb-a $duration $easing infinite; }
.ck.is-burst .ck__icon,
.ck.is-burst .ck__crumb { animation-iteration-count: 1; }

08Scroll-to-end consent gate

The consent button stays disabled until the terms box reaches its end, and the bar above reads the real scrollTop ratio through --p and fills with scaleX. The idle loop never touches the scroll position: it drifts the inner column upward while the bar and the button follow on the same beat.

scaleX 0→1트랙만 translateYradius 12
sgBox.addEventListener('scroll', function () {
  sg.classList.remove('is-demo');
  var max = sgBox.scrollHeight - sgBox.clientHeight;
  var p = max > 0 ? sgBox.scrollTop / max : 1;
  sg.style.setProperty('--p', String(p < 1 ? p : 1));
  if (sgBox.scrollTop + sgBox.clientHeight >= sgBox.scrollHeight - 4) { sgReady(); }
});

09Settings saved toast

The toast enters from the right, waits while a thin timer line shrinks toward its right edge, and exits the way it came. The count and the clock time land on the second line, so pressing save again visibly changes the text.

translateX 120%→0scaleX 1→0 타이머radius 12
@keyframes ts-slide {
  0%   { transform: translateX(120%); }
  14%  { transform: translateX(0); }
  86%  { transform: translateX(0); }
  100% { transform: translateX(120%); }
}
@keyframes ts-timer {
  0%   { transform: scaleX(1); }
  14%  { transform: scaleX(1); }
  86%  { transform: scaleX(0); }
  99%  { transform: scaleX(0); }
  100% { transform: scaleX(1); }
}

Where it breaks — the trap

Number 08 held things up the longest. It sat comfortably inside 480×300 yet reported a document height of 208px in the 320×200 render, spilling by 8px, and the cause was a narrow-screen block that had trimmed the terms box only as far as 88px. Dropping the box to 76px and pulling the card padding in by one step turned the same measurement into a clean pass. Banner-shaped layouts stack a lot of pieces vertically, so a few pixels leak easily on a phone, and measuring the real document height in a narrow viewport beats eyeballing it. The second was the timer line in number 09. A line left at scaleX(0) snaps back to a full bar at the start of the next lap, so the rewind is hidden at the 99% mark while the toast already sits offscreen, which makes 0% and 100% identical. The third was not code at all. All nine share a single demo phrase dictionary, and both the status chip in 01 and the button in 04 used the same Korean word for settings even though one means a stored value and the other means opening a detail view, and no single English string could cover both. Renaming the 01 label so that the two keys split apart is what finally kept the three languages in step. The nine folders behind that fix open with the zip password r26nqrbj, and the React port mirrors the same markup as CSS modules. Number 03 reports 67.8% of its pixels as changed because the backdrop covers the whole frame, which is what a modal is supposed to do, not a defect.

Accessibility and reduced-motion

All nine switch off the looping animation and the transitions under prefers-reduced-motion: reduce, and every click still behaves as before. 01, 04, and 05 hold the banner or card in its arrived position, 03 holds the modal open, and 08 holds the progress bar full. 06 parks the focus ring on the reject button, 07 leaves the crumbs where they scattered, and 09 leaves the toast in view. 02 has no arrived state worth keeping, so it sits in its collapsed shape and changes only when a person presses something. Every control is a real button or checkbox input and reachable by tab, switches use role="switch" with aria-checked, disclosures use aria-expanded, and the modal in 03 uses role="dialog" with aria-modal. The toast in 09 carries role="status" and aria-live="polite", so the confirmation is announced without looking. The exact query conditions are spelled out in the MDN prefers-reduced-motion page.

FAQ

Does pasting this make my site GDPR compliant?

No. What you get here is the interface and the interaction; the law also asks you to hold analytics and advertising scripts back until consent arrives, and to keep a record of that consent. These nine only place a value in localStorage, so a real deployment has to move that value to a cookie or a server and tie script loading to it.

Why does my page content jump down when the banner appears?

Because the banner lives in the document flow and grows a real height. The five here that overlay a mock page — 01, 03, 04, 05, and 09 — float the banner with position: absolute and move it with transform alone, so layout is never recalculated when it appears. The MDN translateY page covers why that property is cheap.

Would a smaller reject button raise my acceptance rate?

In the short run yes, and that is exactly what regulators watch for. Number 06 gives both buttons the same size and weight for that reason, and the sentence above them saying the site still works after a refusal is part of the same pattern.

Need more of the same surfaces? Modal animations covers the opening and closing side, toast notifications covers other shapes of the disappearing notice, and toggle switches has the switch on its own.

Enter the archive password

The password is inside this article. You will find it as you read.