Raxx · internal docs

internal · gated

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
P1 desktop P1 mobile P1 post-consent

P2 — Inline Inset Card

Desktop (compact) Desktop (prefs expanded) Mobile
P2 desktop P2 expanded P2 mobile

P3 — Top Strip + Expansion

Desktop (strip only) Desktop (prefs open) Mobile
P3 desktop P3 prefs open P3 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:


Dark mode story

All three variants are dark-native (CE ink palette). There is no light-mode variant required because:

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.


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.