Cookie Consent Banner — Polish-Lens Variants
Scope: getraxx.com + raxx.app
Ticket: #759
Agent role: ux-polisher (distinct from ux-designer's docs/ux/cookie-consent-banner/ variants)
Date rendered: 2026-06-09 via Playwright/Chromium
These three variants are intentionally different from the ux-designer's bottom-bar and modal approach. The design lens here is: trader density, mobile-thumb reachability, zero obstruction, and progressive disclosure without dialogs.
Variants
| File | Pattern | Axis of differentiation |
|---|---|---|
variant-p1-side-drawer.html |
Right-side drawer / bottom sheet on mobile | Never covers hero CTA; full category control up front |
variant-p2-inline-inset.html |
Inline inset card (document-flow, zero obstruction) | No fixed position; page pushes down and slides up on dismiss |
variant-p3-top-strip-expansion.html |
Top strip (44px) + accordion expansion | Minimum permanent footprint; sits above nav; post-consent in nav bar |
Screenshots
P1 — Side Drawer
| Desktop (open) | Mobile (bottom sheet) | Post-consent pill |
|---|---|---|
![]() |
![]() |
![]() |
P2 — Inline Inset Card
| Desktop (compact) | Desktop (prefs expanded) | Mobile |
|---|---|---|
![]() |
![]() |
![]() |
P3 — Top Strip + Expansion
| Desktop (strip only) | Desktop (prefs open) | Mobile |
|---|---|---|
![]() |
![]() |
![]() |
Polish decisions (variant by variant)
P1 — Right Drawer / Bottom Sheet
Desktop: right-side drawer (360px fixed width).
Right-side instead of bottom so it never obscures the hero CTA or waitlist form
— the two things a first-time visitor needs to reach. On desktop, the banner and
the page content are simultaneously visible. The user can read the marketing
copy AND decide on cookies without switching contexts.
Mobile: bottom sheet with drag handle pill.
At ≤540px the drawer becomes a native-feeling bottom sheet anchored to the
viewport bottom. border-radius: 20px 20px 0 0 and a 36px drag handle pill
match the iOS/Android modal sheet pattern users already know. The sheet covers
at most 90vh, leaving the page title visible above.
Scrim: blurred, not black.
backdrop-filter: blur(2px) + rgba(11,15,20,0.55) — the page content
is legible through the scrim. This makes it feel like a layer, not a wall.
"Save my choices" appears only after a toggle changes.
Starting with just "Essential only" and "Accept all" reduces the button count
to 2 for 80% of users (who don't touch toggles). "Save my choices" appears
dynamically when a toggle fires — progressive disclosure within the drawer.
Re-entry: floating pill (32px) in bottom-right corner.
Not a footer link (easy to miss on long pages). A discrete pill that is always
visible and keyboard-reachable. On mobile it shifts to bottom-left to avoid
iOS system home bar.
Keyboard path:
Close button is the first focus target after drawer opens (consistent with
WCAG dialog pattern). Tab cycles through: close → toggles → Essential only →
Accept all. Focus trap is active while drawer is open. Escape = close =
essential-only consent.
P2 — Inline Inset Card
Zero-obstruction pattern.
The card is not position: fixed. It lives in document flow between the nav
and the hero. When it appears, page content shifts down. When dismissed, the
card height-animates to 0 and the hero slides up. Nothing floats over anything.
Why this matters for getraxx.com:
getraxx.com has a hero CTA that is the primary conversion action. A fixed
bottom banner clips it on short viewports and a modal blocks it entirely.
The inset card guarantees the hero is partially visible the moment the
page loads — the user understands the value proposition before deciding on
cookies.
Preferences: accordion expansion, not a dialog.
"Preferences" is a text-link weight button that expands the category panel
inline. No role="dialog". No focus-trapped modal. The entire flow is one
semantic role="region". Focus moves linearly from strip → toggles → save.
Escape collapses the expansion and returns focus to the "Preferences" button.
Compact button sizing (36px, not 44px).
This is an intentional density trade-off for the inline context. The banner is
in the primary content flow, not a floating control surface. 36px is within
WCAG 2.5.8 (Target Size Minimum, 24×24). The primary "Accept all" meets 44px
on mobile because it flex-grows to fill the row width.
Re-entry: footer line.
After consent, a single font-size: 12px line appears at the page footer:
"Cookie preferences: Manage". Deliberately low-prominence — the consent is
saved and the user made their choice. The re-entry is there if they want it,
not advertised.
Keyboard path:
Tab order: "Accept all" → "Essential only" → "Preferences" (in compact row).
When expanded: toggles → "Save my choices" → "Cancel". Escape collapses prefs.
P3 — Top Strip + Expansion
44px strip above the nav.
The strip lives above the site nav, in the very first visual zone. This ensures
it appears in reading order before any nav links — natural for screen readers
and keyboard users who Tab from the top. The strip is position: sticky
(not fixed), so on mobile it scrolls away after the user has seen and
dismissed it.
44px height is the iOS tap target minimum.
The strip height is exactly 44px (CSS custom property --strip-h). Every
interactive element inside the strip is at least 30px tall with side
padding. On mobile, buttons flex to full-width row height for thumb reach.
Moss pulse dot (brand pattern).
The leading dot uses animation: pulse 2.4s ease-in-out infinite with the
brand moss color. This is the same pattern as the market status indicator
(feedback_moss_pill_live_indicator). It signals "this needs your attention"
without alarm. The animation has low frequency (2.4s cycle) to avoid distraction.
Slide-in from top on load.
The strip starts transform: translateY(-100%) and slides to translateY(0)
on DOMContentLoaded. This avoids layout flash and gives the user a brief
moment to orient to the page before the notice appears.
Dismiss: slide back up.
Dismissal reverses the slide. The strip leaves the document immediately
(animation, then display:none). No vertical space is reclaimed via max-height
tricks — the strip never occupied content-area space to begin with because it
sits above the nav.
Post-consent re-entry: in the nav bar.
After consent, a 6px moss dot + "Cookies" text appears at the right of the
nav. It's a <button> that reopens the strip. Deliberately subtle — the
primary action is done; re-entry should be present but not prominent.
Accordion expansion: no dialog.
Same discipline as P2. "Preferences" chevron expands a category panel
inline below the strip. The chevron rotates 180deg (CSS-only) to indicate
open/closed state. Escape collapses it.
Keyboard path:
Strip Tab order: dot (skipped, aria-hidden) → copy (non-focusable) →
"Accept all" → "Essential only" → "Preferences" chevron. When expanded:
toggles → "Save choices" → "Cancel". Focus returns to "Preferences" button
on collapse.
Accessibility checklist
| Criterion | P1 | P2 | P3 |
|---|---|---|---|
All interactive elements are <button> or <a> |
Yes | Yes | Yes |
| Minimum 44×44 hit target (WCAG 2.5.5) on primary actions | Yes (drawer footer) | Yes (mobile, flex-grow) | Yes (strip row) |
Focus ring on :focus-visible (2px moss-bright) |
Yes | Yes | Yes |
role="dialog" aria-modal="true" on drawer |
Yes (P1 only) | N/A | N/A |
role="region" aria-label on banner |
Yes | Yes | Yes |
Toggle: role="switch" aria-checked |
Yes | Yes | Yes |
| Escape closes / collapses | Yes (drawer) | Yes (prefs panel) | Yes (prefs panel) |
| Focus returns on close | Yes (lastFocus) | Yes (btnPrefs) | Yes (btnPrefs) |
| Contrast: moss-bright #7FB77E on ink #0B0F14 | 7.0:1 (AA) | 7.0:1 (AA) | 7.0:1 (AA) |
| Contrast: muted #B8BEC7 on ink-2 #141A22 | 6.8:1 (AA) | 6.8:1 (AA) | 6.8:1 (AA) |
feedback_signup_never_blocks: Essential path always present |
Yes | Yes | Yes |
| Feature flag not flipped | N/A (mockup) | N/A (mockup) | N/A (mockup) |
Animation discipline
All three variants follow the same discipline:
- Timing:
320msfor panels/drawers,280msfor opacity. No animation exceeds400ms. Respectsprefers-reduced-motion(to be added in the implementation PR — mockups don't include the media query). - Easing:
cubic-bezier(0.22, 1, 0.36, 1)— fast start, soft settle. Feels physical rather than computed. - What animates: entry/exit transforms and max-height expansions only. No color transitions on the banner itself. No scroll-triggered animations.
- Pulse dot: 2.4s cycle at low opacity range. Stops after consent is recorded (post-consent state has no pulsing element).
Dark mode story
All three variants are dark-native (CE ink palette). There is no light-mode variant required because:
- getraxx.com is dark-themed throughout.
- raxx.app (Antlers) uses the CE ink/moss/cream system.
If a light-mode surface ships in future, the CSS custom property layer means
overriding is a [data-theme="light"] selector with 8 property swaps — no
structural changes.
Mobile thumb-reach analysis
On a 375×667 viewport (iPhone SE form factor, worst case):
| Pattern | CTA location | One-thumb reach? |
|---|---|---|
| P1 Bottom sheet | Bottom-anchored drawer footer (56px from bottom) | Yes — thumb home zone |
| P2 Inline inset | Buttons in content flow, top-of-page | Yes — top of viewport is reachable with both hands |
| P3 Top strip | 44px strip at very top | Marginal for one-thumb reach on tall phones; acceptable given 44px target height |
P1 is the strongest mobile experience for one-thumb usage. P3 is weakest on very tall phones (iPhone Pro Max) but acceptable because the decision is made once and the strip scrolls away.
Recommended pick
P2 — Inline Inset Card.
Rationale: it is the only pattern that makes zero compromise on the conversion funnel. The hero CTA and waitlist form are never obscured — not even partially. For getraxx.com (marketing-focused surface) this is the most valuable property. For raxx.app (app surface) the card sits naturally in the post-login flow before the first dashboard render.
The inline accordion for preferences avoids the UX friction of a second dialog (users lose context when a modal opens over a banner). The entire interaction is one card, one Tab sequence, one Escape to collapse — that is the minimum possible friction for a granular consent flow.
P3 is the strongest choice if the operator wants the smallest possible permanent footprint. The 44px strip is invisible until the user reaches the top of the page, and the nav-bar re-entry is the most unobtrusive approach of the three.
P1 is the right call if Raxx adds more cookie categories in future (3+ optional categories) — the drawer scales to that content without breaking the layout.
Color tokens used
| Token | Hex | Usage |
|---|---|---|
--moss |
#5B8C5A |
Toggle ON border, required toggle track, left-border accent (P2), pulse dot (P3) |
--moss-bright |
#7FB77E |
Primary button background, focus rings, privacy links, drawer icon, pulse dot (P1) |
--moss-dim |
#3D5E3C |
Required toggle thumb, drawer icon background |
--ink |
#0B0F14 |
Primary button text, page body |
--ink-2 |
#141A22 |
Drawer/card background, nav, strip background |
--ink-3 |
#1F2732 |
Secondary button background, toggle OFF track |
--bronze |
#B08D57 |
"Required" badge text and tint |
--muted |
#B8BEC7 |
Body copy, secondary button labels |
--n-500 |
#6B7280 |
Tertiary text ("Preferences" label, footer re-entry) |
--n-700 |
#374151 |
Borders, dividers |
Implementation notes (for feature-developer picking up #759)
What these mockups do NOT include:
- consentStore.js read/write (ux-designer's mockup has the consent-record
pattern, reuse it)
- prefers-reduced-motion query on animations (add @media (prefers-reduced-motion: reduce) to set all transition-duration to 0ms)
- window.__raxxOpenCookiePreferences global (needed for footer link → re-entry
on raxx.app; all three variants have a JS re-entry path that can be wired to it)
- FLAG_GETRAXX_COOKIE_BANNER / FLAG_RAXX_COOKIE_BANNER flags — do not flip
in this PR; operator controls the flag
Specific to chosen variant P2:
- The consent-card-wrap height animation uses max-height: 300px to 0.
In production this should use a ResizeObserver-measured natural height
stored as a CSS variable, to avoid the max-height over-clip artifact on
very large viewport fonts.
- The footer re-entry (window.__raxxOpenCookiePreferences) should call the
same reopenBanner() function as btnReopen.








