TLDR: Cookie banner design directly impacts consent rates—our research of 2.3M interactions reveals what actually works.
Read full summary
Data-driven analysis of cookie banner effectiveness. Center modals outperform bottom bars (71% vs 62% acceptance), green buttons boost consent 12%, and mobile-first design is essential. Learn the psychology behind high-converting, compliant banners with production-ready TypeScript implementations for A/B testing and consent rate optimization.
*Summary by Claude AI*
## What is the best placement for a cookie banner?
According to our research of 2.3 million interactions, a **center modal** yields the highest acceptance rates (71% desktop, 77% mobile) because it demands user attention, whereas bottom bars are often ignored or result in bounce. This comprehensive guide explores the psychology behind consent decisions and provides data-driven strategies for optimizing your consent experience.
## Introduction
Cookie banner design isn't just about compliance—it's about psychology. The way you present consent choices fundamentally shapes user behavior, affecting everything from acceptance rates to trust signals to conversion metrics.
Our research team analyzed 2.3 million cookie banner interactions across 847 websites over 18 months to understand what drives consent decisions. The findings challenge conventional wisdom and provide actionable insights for optimizing your consent experience while remaining fully compliant.
This guide combines behavioral psychology principles with rigorous A/B testing data to help you design consent interfaces that work for both your users and your business.
## The Psychology of Consent Decisions
Understanding why users make the choices they do is fundamental to ethical consent optimization.
```typescript
// consent-psychology-engine.ts
// Apply behavioral psychology principles to consent UX
interface PsychologyPrinciple {
name: string;
description: string;
application: string;
ethicalBoundary: string;
impactOnConsent: number; // -1 to 1 multiplier
}
interface UserBehaviorProfile {
attentionSpan: 'short' | 'medium' | 'long';
decisionStyle: 'quick' | 'deliberate';
trustLevel: 'low' | 'medium' | 'high';
privacyConcern: 'low' | 'medium' | 'high';
deviceType: 'mobile' | 'tablet' | 'desktop';
}
interface ConsentPrediction {
likelyToAccept: number; // 0-1 probability
likelyToReject: number; // 0-1 probability
likelyToCustomize: number; // 0-1 probability
likelyToBounce: number; // 0-1 probability
recommendedApproach: string;
}
class ConsentPsychologyEngine {
private readonly principles: PsychologyPrinciple[] = [
{
name: 'Cognitive Load Reduction',
description: 'Users prefer simpler choices that require less mental effort',
application: 'Present clear, binary choices instead of complex preference grids',
ethicalBoundary: 'Must not hide important options to appear simpler',
impactOnConsent: 0.15
},
{
name: 'Decision Fatigue',
description: 'Users make worse decisions after many prior choices',
application: 'Show consent early in the session, not after complex flows',
ethicalBoundary: 'Cannot exploit fatigue to obtain consent',
impactOnConsent: 0.10
},
{
name: 'Loss Aversion',
description: 'Users fear losing access more than they value gaining features',
application: 'Frame consent as enabling personalization, not as losing privacy',
ethicalBoundary: 'Cannot imply rejection causes loss of core functionality',
impactOnConsent: 0.20
},
{
name: 'Social Proof',
description: 'Users follow perceived majority behavior',
application: 'Show how many users have made consent choices',
ethicalBoundary: 'Cannot fabricate or manipulate social proof statistics',
impactOnConsent: 0.08
},
{
name: 'Trust Transfer',
description: 'Users extend trust from known brands to unknown elements',
application: 'Prominently display brand identity in consent interface',
ethicalBoundary: 'Brand must accurately represent consent practices',
impactOnConsent: 0.12
},
{
name: 'Reciprocity',
description: 'Users feel obligated to reciprocate value received',
application: 'Explain the value exchange before asking for consent',
ethicalBoundary: 'Cannot condition content access on consent',
impactOnConsent: 0.18
},
{
name: 'Autonomy Respect',
description: 'Users respond positively to feeling in control',
application: 'Emphasize user choice and control in copy',
ethicalBoundary: 'Must provide genuine, meaningful choices',
impactOnConsent: 0.25
}
];
// Predict user consent behavior based on profile
predictBehavior(profile: UserBehaviorProfile): ConsentPrediction {
let acceptProbability = 0.65; // Baseline
let rejectProbability = 0.20;
let customizeProbability = 0.10;
let bounceProbability = 0.05;
// Adjust for device type
if (profile.deviceType === 'mobile') {
acceptProbability += 0.05; // Mobile users often accept faster
customizeProbability -= 0.05; // Less likely to dig into settings
}
// Adjust for privacy concern
if (profile.privacyConcern === 'high') {
acceptProbability -= 0.25;
rejectProbability += 0.15;
customizeProbability += 0.10;
} else if (profile.privacyConcern === 'low') {
acceptProbability += 0.10;
rejectProbability -= 0.08;
}
// Adjust for trust level
if (profile.trustLevel === 'high') {
acceptProbability += 0.15;
rejectProbability -= 0.10;
} else if (profile.trustLevel === 'low') {
acceptProbability -= 0.20;
rejectProbability += 0.10;
bounceProbability += 0.10;
}
// Adjust for decision style
if (profile.decisionStyle === 'quick') {
customizeProbability -= 0.05;
bounceProbability += 0.03; // Quick deciders may bounce if annoyed
} else {
customizeProbability += 0.08;
bounceProbability -= 0.02;
}
// Normalize probabilities
const total = acceptProbability + rejectProbability + customizeProbability + bounceProbability;
acceptProbability /= total;
rejectProbability /= total;
customizeProbability /= total;
bounceProbability /= total;
// Generate recommendation
let recommendedApproach: string;
if (profile.privacyConcern === 'high') {
recommendedApproach = 'Lead with privacy-first messaging; highlight granular controls';
} else if (profile.trustLevel === 'low') {
recommendedApproach = 'Build trust first; emphasize security and data protection';
} else if (profile.decisionStyle === 'quick' && profile.deviceType === 'mobile') {
recommendedApproach = 'Streamlined mobile-optimized UI; minimize friction';
} else {
recommendedApproach = 'Standard consent flow with clear value proposition';
}
return {
likelyToAccept: Math.round(acceptProbability * 100) / 100,
likelyToReject: Math.round(rejectProbability * 100) / 100,
likelyToCustomize: Math.round(customizeProbability * 100) / 100,
likelyToBounce: Math.round(bounceProbability * 100) / 100,
recommendedApproach
};
}
// Get applicable psychology principles for a context
getApplicablePrinciples(
context: 'first_visit' | 'returning_user' | 'checkout' | 'content'
): PsychologyPrinciple[] {
const contextWeights: Record = {
first_visit: ['Trust Transfer', 'Cognitive Load Reduction', 'Autonomy Respect'],
returning_user: ['Reciprocity', 'Social Proof'],
checkout: ['Decision Fatigue', 'Loss Aversion'],
content: ['Reciprocity', 'Autonomy Respect', 'Cognitive Load Reduction']
};
const relevantNames = contextWeights[context] || [];
return this.principles.filter(p => relevantNames.includes(p.name));
}
// Generate ethical consent copy based on principles
generateEthicalCopy(principles: PsychologyPrinciple[]): ConsentCopyVariants {
const variants: ConsentCopyVariants = {
title: [],
description: [],
acceptText: [],
rejectText: []
};
if (principles.some(p => p.name === 'Cognitive Load Reduction')) {
variants.title.push('Your Privacy Choices');
variants.description.push('We use cookies to improve your experience. Choose your preferences below.');
}
if (principles.some(p => p.name === 'Autonomy Respect')) {
variants.title.push('You\'re in Control');
variants.description.push('Choose how we can enhance your experience. You can change your mind anytime.');
}
if (principles.some(p => p.name === 'Trust Transfer')) {
variants.title.push('Privacy at [Brand]');
variants.description.push('As part of our commitment to your privacy, we ask for your consent.');
}
if (principles.some(p => p.name === 'Reciprocity')) {
variants.description.push('To provide free content and personalized recommendations, we use cookies.');
}
// Standard ethical button text
variants.acceptText = ['Accept All', 'Allow Cookies', 'Accept'];
variants.rejectText = ['Decline All', 'Reject', 'No Thanks'];
return variants;
}
// Validate that an approach stays within ethical boundaries
validateEthicalCompliance(approach: ConsentApproach): EthicalValidation {
const violations: string[] = [];
const warnings: string[] = [];
// Check for dark patterns
if (approach.rejectButtonSize < approach.acceptButtonSize * 0.7) {
violations.push('Reject button significantly smaller than accept - visual asymmetry');
}
if (approach.rejectRequiresMoreClicks) {
violations.push('Reject requires more clicks than accept - obstruction');
}
if (approach.preCheckedNonEssential) {
violations.push('Non-essential options pre-checked - illegal default');
}
if (approach.usesShameLanguage) {
violations.push('Shame language in reject option - confirm-shaming');
}
// Check for warnings (not violations but risky)
if (approach.hideRejectBehindLink) {
warnings.push('Reject hidden behind "manage preferences" - consider equal prominence');
}
if (approach.showsOnlyAcceptInitially) {
warnings.push('Only accept visible initially - may violate DSA equal prominence');
}
if (approach.usesEmotionalFraming) {
warnings.push('Emotional framing detected - ensure not manipulative');
}
return {
compliant: violations.length === 0,
violations,
warnings,
riskLevel: violations.length > 0 ? 'high' :
warnings.length > 2 ? 'medium' : 'low'
};
}
}
interface ConsentCopyVariants {
title: string[];
description: string[];
acceptText: string[];
rejectText: string[];
}
interface ConsentApproach {
acceptButtonSize: number;
rejectButtonSize: number;
rejectRequiresMoreClicks: boolean;
preCheckedNonEssential: boolean;
usesShameLanguage: boolean;
hideRejectBehindLink: boolean;
showsOnlyAcceptInitially: boolean;
usesEmotionalFraming: boolean;
}
interface EthicalValidation {
compliant: boolean;
violations: string[];
warnings: string[];
riskLevel: 'low' | 'medium' | 'high';
}
export { ConsentPsychologyEngine, UserBehaviorProfile, ConsentPrediction };
```
## Research Findings: Placement Impact
Our analysis revealed surprising placement effects on consent rates:
```typescript
// placement-research-analyzer.ts
// Analyze placement impact on consent rates
interface PlacementData {
placement: 'bottom_bar' | 'top_bar' | 'center_modal' | 'corner_popup' | 'full_screen';
desktopAcceptance: number;
mobileAcceptance: number;
avgTimeToDecision: number; // seconds
bounceRate: number;
customizationRate: number;
sampleSize: number;
}
interface ResearchFindings {
data: PlacementData[];
insights: string[];
recommendations: PlacementRecommendation[];
}
interface PlacementRecommendation {
scenario: string;
recommended: string;
reasoning: string;
expectedImpact: string;
}
class PlacementResearchAnalyzer {
// Our research data from 2.3M interactions
private readonly researchData: PlacementData[] = [
{
placement: 'bottom_bar',
desktopAcceptance: 0.62,
mobileAcceptance: 0.54,
avgTimeToDecision: 4.2,
bounceRate: 0.08,
customizationRate: 0.12,
sampleSize: 680000
},
{
placement: 'top_bar',
desktopAcceptance: 0.58,
mobileAcceptance: 0.49,
avgTimeToDecision: 3.8,
bounceRate: 0.11,
customizationRate: 0.09,
sampleSize: 420000
},
{
placement: 'center_modal',
desktopAcceptance: 0.71,
mobileAcceptance: 0.77,
avgTimeToDecision: 2.1,
bounceRate: 0.06,
customizationRate: 0.08,
sampleSize: 890000
},
{
placement: 'corner_popup',
desktopAcceptance: 0.65,
mobileAcceptance: 0.61,
avgTimeToDecision: 5.3,
bounceRate: 0.05,
customizationRate: 0.15,
sampleSize: 230000
},
{
placement: 'full_screen',
desktopAcceptance: 0.74,
mobileAcceptance: 0.79,
avgTimeToDecision: 1.8,
bounceRate: 0.12,
customizationRate: 0.05,
sampleSize: 80000
}
];
analyzeFindings(): ResearchFindings {
const insights = this.generateInsights();
const recommendations = this.generateRecommendations();
return {
data: this.researchData,
insights,
recommendations
};
}
private generateInsights(): string[] {
return [
'Center modals achieve 9-15% higher acceptance than bottom bars across devices',
'Mobile users show 6% higher acceptance with center modals vs desktop (77% vs 71%)',
'Full-screen interstitials have highest acceptance but also highest bounce rate',
'Corner popups have lowest bounce rate but also highest time-to-decision',
'Top bars consistently underperform other placements by 4-13%',
'Mobile bottom bars have 8% lower acceptance than desktop (54% vs 62%)',
'Time-to-decision inversely correlates with modal visibility (r=-0.82)',
'Customization rate is highest with corner popups (15%) and lowest with full-screen (5%)'
];
}
private generateRecommendations(): PlacementRecommendation[] {
return [
{
scenario: 'E-commerce site prioritizing conversion',
recommended: 'center_modal',
reasoning: 'Balances high acceptance (71-77%) with low bounce rate (6%)',
expectedImpact: '+9-15% consent rate vs bottom bar'
},
{
scenario: 'Content publisher with ad-supported model',
recommended: 'center_modal',
reasoning: 'Maximizes consent for advertising without excessive bounce',
expectedImpact: '+12% ad-eligible traffic'
},
{
scenario: 'B2B SaaS with complex privacy requirements',
recommended: 'corner_popup',
reasoning: 'Higher customization rate allows granular consent',
expectedImpact: '+15% users engaging with preferences'
},
{
scenario: 'Mobile-first application',
recommended: 'center_modal',
reasoning: 'Strongest mobile performance (77%), thumb-friendly',
expectedImpact: '+23% consent rate vs mobile bottom bar'
},
{
scenario: 'News/media site with bounce sensitivity',
recommended: 'corner_popup',
reasoning: 'Lowest bounce rate (5%) while maintaining reasonable consent',
expectedImpact: '-3% bounce rate vs center modal'
}
];
}
// Compare two placements
comparePlacements(a: PlacementData['placement'], b: PlacementData['placement']): PlacementComparison {
const dataA = this.researchData.find(d => d.placement === a)!;
const dataB = this.researchData.find(d => d.placement === b)!;
const desktopDiff = dataA.desktopAcceptance - dataB.desktopAcceptance;
const mobileDiff = dataA.mobileAcceptance - dataB.mobileAcceptance;
const bounceDiff = dataA.bounceRate - dataB.bounceRate;
return {
placement1: a,
placement2: b,
desktopAcceptanceDiff: `${(desktopDiff * 100).toFixed(0)}%`,
mobileAcceptanceDiff: `${(mobileDiff * 100).toFixed(0)}%`,
bounceRateDiff: `${(bounceDiff * 100).toFixed(1)}%`,
winner: desktopDiff + mobileDiff > 0 ? a : b,
significance: this.calculateSignificance(dataA, dataB)
};
}
private calculateSignificance(a: PlacementData, b: PlacementData): 'high' | 'medium' | 'low' {
// Simplified significance based on sample size and effect size
const minSample = Math.min(a.sampleSize, b.sampleSize);
const effectSize = Math.abs(a.desktopAcceptance - b.desktopAcceptance);
if (minSample > 100000 && effectSize > 0.05) return 'high';
if (minSample > 50000 && effectSize > 0.03) return 'medium';
return 'low';
}
// Get placement data for visualization
getVisualizationData(): PlacementVisualization {
return {
labels: this.researchData.map(d => d.placement.replace('_', ' ')),
desktopData: this.researchData.map(d => Math.round(d.desktopAcceptance * 100)),
mobileData: this.researchData.map(d => Math.round(d.mobileAcceptance * 100)),
bounceData: this.researchData.map(d => Math.round(d.bounceRate * 100))
};
}
}
interface PlacementComparison {
placement1: string;
placement2: string;
desktopAcceptanceDiff: string;
mobileAcceptanceDiff: string;
bounceRateDiff: string;
winner: string;
significance: 'high' | 'medium' | 'low';
}
interface PlacementVisualization {
labels: string[];
desktopData: number[];
mobileData: number[];
bounceData: number[];
}
export { PlacementResearchAnalyzer, PlacementData, ResearchFindings };
```
## Color Psychology in Consent Design
Button colors significantly impact consent rates—but the effects are more nuanced than most assume:
```typescript
// color-psychology-optimizer.ts
// Optimize consent UI colors based on psychological research
interface ColorTestResult {
acceptButtonColor: string;
rejectButtonColor: string;
acceptanceRate: number;
rejectionRate: number;
customizeRate: number;
sampleSize: number;
context: 'ecommerce' | 'content' | 'saas' | 'general';
}
interface ColorRecommendation {
acceptColor: string;
rejectColor: string;
reasoning: string;
expectedLift: string;
a11yCompliant: boolean;
}
class ColorPsychologyOptimizer {
// Research data on color combinations
private readonly colorResearch: ColorTestResult[] = [
{
acceptButtonColor: '#22c55e', // Green
rejectButtonColor: '#6b7280', // Gray
acceptanceRate: 0.72,
rejectionRate: 0.18,
customizeRate: 0.10,
sampleSize: 125000,
context: 'ecommerce'
},
{
acceptButtonColor: '#3b82f6', // Blue
rejectButtonColor: '#6b7280', // Gray
acceptanceRate: 0.68,
rejectionRate: 0.21,
customizeRate: 0.11,
sampleSize: 98000,
context: 'saas'
},
{
acceptButtonColor: '#8b5cf6', // Purple
rejectButtonColor: '#6b7280', // Gray
acceptanceRate: 0.65,
rejectionRate: 0.22,
customizeRate: 0.13,
sampleSize: 45000,
context: 'content'
},
{
acceptButtonColor: '#ef4444', // Red accept
rejectButtonColor: '#6b7280', // Gray
acceptanceRate: 0.58,
rejectionRate: 0.28,
customizeRate: 0.14,
sampleSize: 32000,
context: 'general'
},
{
acceptButtonColor: '#22c55e', // Green accept
rejectButtonColor: '#ef4444', // Red reject
acceptanceRate: 0.75,
rejectionRate: 0.15,
customizeRate: 0.10,
sampleSize: 87000,
context: 'ecommerce'
},
{
acceptButtonColor: '#3b82f6', // Blue (both same)
rejectButtonColor: '#3b82f6', // Blue (equal prominence)
acceptanceRate: 0.64,
rejectionRate: 0.24,
customizeRate: 0.12,
sampleSize: 156000,
context: 'general'
}
];
// Color psychology principles
private readonly colorMeanings: Record = {
green: {
associations: ['go', 'positive', 'growth', 'approval'],
psychologicalEffect: 'Signals positive action, safety',
consentImpact: '+12% acceptance when used for accept button',
warnings: ['Can feel manipulative if reject is significantly less prominent']
},
blue: {
associations: ['trust', 'calm', 'professional', 'security'],
psychologicalEffect: 'Builds trust, feels safe and reliable',
consentImpact: '+8% acceptance in B2B/SaaS contexts',
warnings: ['May feel cold or impersonal']
},
red: {
associations: ['stop', 'danger', 'warning', 'attention'],
psychologicalEffect: 'Creates urgency but also caution',
consentImpact: '-8% rejection rate when used for reject button',
warnings: ['Users avoid red buttons - can be manipulative for reject']
},
gray: {
associations: ['neutral', 'secondary', 'inactive'],
psychologicalEffect: 'Feels less important, secondary',
consentImpact: 'Standard for secondary actions',
warnings: ['Gray reject button may violate equal prominence requirements']
},
purple: {
associations: ['premium', 'creative', 'wisdom'],
psychologicalEffect: 'Feels sophisticated, creative',
consentImpact: 'Effective for creative/content sites',
warnings: ['May not fit all brand contexts']
}
};
// Generate compliant color recommendation
getRecommendation(
context: ColorTestResult['context'],
brandColor: string,
requireEqualProminence: boolean
): ColorRecommendation {
if (requireEqualProminence) {
// DSA/CPRA compliant: both buttons same color
return {
acceptColor: brandColor,
rejectColor: brandColor,
reasoning: 'Equal prominence required by regulation. Both buttons use brand color.',
expectedLift: 'Baseline (compliant design)',
a11yCompliant: this.checkA11yCompliance(brandColor, '#ffffff')
};
}
// Find best performing color for context
const contextData = this.colorResearch.filter(r => r.context === context);
const bestResult = contextData.sort((a, b) => b.acceptanceRate - a.acceptanceRate)[0];
if (bestResult) {
return {
acceptColor: bestResult.acceptButtonColor,
rejectColor: this.getSafeRejectColor(bestResult.rejectButtonColor),
reasoning: this.generateReasoning(bestResult),
expectedLift: `+${Math.round((bestResult.acceptanceRate - 0.64) * 100)}% vs neutral baseline`,
a11yCompliant: this.checkA11yCompliance(bestResult.acceptButtonColor, '#ffffff')
};
}
// Default recommendation
return {
acceptColor: '#3b82f6',
rejectColor: '#3b82f6',
reasoning: 'Blue is universally trusted and works across contexts',
expectedLift: '+8% vs neutral gray',
a11yCompliant: true
};
}
private getSafeRejectColor(color: string): string {
// Don't return red for reject - it's manipulative
if (color === '#ef4444') {
return '#6b7280'; // Gray instead
}
return color;
}
private generateReasoning(result: ColorTestResult): string {
const acceptColor = this.getColorName(result.acceptButtonColor);
const acceptMeaning = this.colorMeanings[acceptColor];
return `${acceptColor.charAt(0).toUpperCase() + acceptColor.slice(1)} accept button ` +
`leverages ${acceptMeaning?.associations.slice(0, 2).join(' and ')} associations. ` +
`${acceptMeaning?.consentImpact || ''}`;
}
private getColorName(hex: string): string {
const colorMap: Record = {
'#22c55e': 'green',
'#3b82f6': 'blue',
'#8b5cf6': 'purple',
'#ef4444': 'red',
'#6b7280': 'gray'
};
return colorMap[hex] || 'custom';
}
private checkA11yCompliance(foreground: string, background: string): boolean {
// Simplified WCAG contrast check
const getLuminance = (hex: string): number => {
const rgb = parseInt(hex.replace('#', ''), 16);
const r = ((rgb >> 16) & 0xff) / 255;
const g = ((rgb >> 8) & 0xff) / 255;
const b = (rgb & 0xff) / 255;
const adjust = (c: number) =>
c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
return 0.2126 * adjust(r) + 0.7152 * adjust(g) + 0.0722 * adjust(b);
};
const l1 = getLuminance(foreground);
const l2 = getLuminance(background);
const ratio = (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
return ratio >= 4.5; // WCAG AA standard
}
// Validate color combination for compliance
validateColorCompliance(
acceptColor: string,
rejectColor: string
): ColorComplianceReport {
const issues: string[] = [];
const warnings: string[] = [];
// Check for red reject button (manipulative)
if (this.getColorName(rejectColor) === 'red') {
issues.push('Red reject button is manipulative - users avoid red');
}
// Check for significant prominence difference
const acceptLuminance = this.getPerceivedBrightness(acceptColor);
const rejectLuminance = this.getPerceivedBrightness(rejectColor);
if (Math.abs(acceptLuminance - rejectLuminance) > 50) {
warnings.push('Significant visual prominence difference between buttons');
}
// Check saturation difference
const acceptSaturation = this.getSaturation(acceptColor);
const rejectSaturation = this.getSaturation(rejectColor);
if (acceptSaturation > 50 && rejectSaturation < 20) {
warnings.push('Accept is vibrant while reject is desaturated - unequal attention');
}
return {
compliant: issues.length === 0,
issues,
warnings,
recommendation: issues.length > 0 ?
'Consider using same color for both buttons' :
warnings.length > 0 ?
'Review color choices for equal prominence' :
'Color combination appears compliant'
};
}
private getPerceivedBrightness(hex: string): number {
const rgb = parseInt(hex.replace('#', ''), 16);
const r = (rgb >> 16) & 0xff;
const g = (rgb >> 8) & 0xff;
const b = rgb & 0xff;
return Math.round((r * 299 + g * 587 + b * 114) / 1000);
}
private getSaturation(hex: string): number {
const rgb = parseInt(hex.replace('#', ''), 16);
const r = ((rgb >> 16) & 0xff) / 255;
const g = ((rgb >> 8) & 0xff) / 255;
const b = (rgb & 0xff) / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const l = (max + min) / 2;
if (max === min) return 0;
const d = max - min;
return Math.round((l > 0.5 ? d / (2 - max - min) : d / (max + min)) * 100);
}
}
interface ColorMeaning {
associations: string[];
psychologicalEffect: string;
consentImpact: string;
warnings: string[];
}
interface ColorComplianceReport {
compliant: boolean;
issues: string[];
warnings: string[];
recommendation: string;
}
export { ColorPsychologyOptimizer, ColorRecommendation };
```
## A/B Testing Framework for Consent Optimization
Systematic testing is essential for optimizing consent rates ethically:
```typescript
// consent-ab-testing-framework.ts
// Comprehensive A/B testing for consent optimization
interface ABTestConfig {
id: string;
name: string;
description: string;
variants: ConsentVariant[];
traffic: number; // 0-1 percentage of traffic
minSampleSize: number;
maxDuration: number; // days
successMetric: 'acceptance_rate' | 'rejection_rate' | 'bounce_rate' | 'composite';
constraints: TestConstraints;
}
interface ConsentVariant {
id: string;
name: string;
weight: number; // 0-1, should sum to 1 across variants
config: VariantConfig;
}
interface VariantConfig {
placement: 'bottom_bar' | 'center_modal' | 'corner_popup' | 'top_bar';
buttonLayout: 'horizontal' | 'stacked';
copyVariant: string;
colorScheme: string;
timing: number; // ms delay before showing
animation: 'none' | 'fade' | 'slide';
}
interface TestConstraints {
minConsentRate: number; // Don't deploy if consent drops below
maxBounceRate: number; // Don't deploy if bounce exceeds
equalProminence: boolean; // Enforce equal button prominence
noShameLanguage: boolean; // Validate copy for shame language
}
interface TestResult {
variantId: string;
sampleSize: number;
acceptanceRate: number;
rejectionRate: number;
bounceRate: number;
avgTimeToDecision: number;
customizationRate: number;
confidenceInterval: [number, number];
statisticallySignificant: boolean;
}
class ConsentABTestingFramework {
private activeTests: Map = new Map();
private testResults: Map> = new Map();
// Create a new A/B test
createTest(config: ABTestConfig): CreateTestResult {
// Validate test configuration
const validation = this.validateTestConfig(config);
if (!validation.valid) {
return { success: false, errors: validation.errors };
}
// Validate all variants for compliance
for (const variant of config.variants) {
const complianceCheck = this.checkVariantCompliance(variant, config.constraints);
if (!complianceCheck.compliant) {
return {
success: false,
errors: [`Variant "${variant.name}" violates constraints: ${complianceCheck.issues.join(', ')}`]
};
}
}
// Register test
this.activeTests.set(config.id, config);
this.testResults.set(config.id, new Map());
return { success: true, testId: config.id };
}
// Assign user to variant
assignVariant(testId: string, userId: string): VariantAssignment | null {
const test = this.activeTests.get(testId);
if (!test) return null;
// Check if within traffic allocation
const userHash = this.hashUserId(userId);
if (userHash > test.traffic) {
return null; // User not in test
}
// Deterministic variant assignment based on user ID
const variantHash = this.hashUserId(userId + testId);
let cumulative = 0;
for (const variant of test.variants) {
cumulative += variant.weight;
if (variantHash <= cumulative) {
return {
testId,
variantId: variant.id,
variantName: variant.name,
config: variant.config
};
}
}
// Fallback to first variant
return {
testId,
variantId: test.variants[0].id,
variantName: test.variants[0].name,
config: test.variants[0].config
};
}
// Record consent event
recordEvent(
testId: string,
variantId: string,
event: ConsentEvent
): void {
const testResults = this.testResults.get(testId);
if (!testResults) return;
let result = testResults.get(variantId);
if (!result) {
result = this.createEmptyResult(variantId);
testResults.set(variantId, result);
}
// Update metrics
result.sampleSize++;
switch (event.action) {
case 'accept_all':
result.acceptanceRate = this.updateRate(
result.acceptanceRate,
result.sampleSize,
1
);
break;
case 'reject_all':
result.rejectionRate = this.updateRate(
result.rejectionRate,
result.sampleSize,
1
);
break;
case 'customize':
result.customizationRate = this.updateRate(
result.customizationRate,
result.sampleSize,
1
);
break;
case 'bounce':
result.bounceRate = this.updateRate(
result.bounceRate,
result.sampleSize,
1
);
break;
}
// Update time to decision
if (event.timeToDecision) {
result.avgTimeToDecision = this.updateAverage(
result.avgTimeToDecision,
result.sampleSize,
event.timeToDecision
);
}
// Update confidence interval
result.confidenceInterval = this.calculateConfidenceInterval(
result.acceptanceRate,
result.sampleSize
);
// Check statistical significance
result.statisticallySignificant = this.checkSignificance(testId);
}
// Analyze test results
analyzeTest(testId: string): TestAnalysis | null {
const test = this.activeTests.get(testId);
const results = this.testResults.get(testId);
if (!test || !results) return null;
const variantResults = Array.from(results.values());
// Find winning variant
const sortedByAcceptance = [...variantResults].sort(
(a, b) => b.acceptanceRate - a.acceptanceRate
);
const winner = sortedByAcceptance[0];
const control = variantResults.find(v => v.variantId === 'control') || sortedByAcceptance[sortedByAcceptance.length - 1];
// Calculate lift
const lift = ((winner.acceptanceRate - control.acceptanceRate) / control.acceptanceRate) * 100;
// Check if test meets minimum requirements
const meetsMinimumSample = variantResults.every(
v => v.sampleSize >= test.minSampleSize
);
const anySignificant = variantResults.some(v => v.statisticallySignificant);
return {
testId,
testName: test.name,
status: this.determineTestStatus(test, variantResults),
results: variantResults,
winner: winner.variantId,
winnerLift: `${lift.toFixed(1)}%`,
recommendation: this.generateRecommendation(test, variantResults, winner, lift),
canConclude: meetsMinimumSample && anySignificant,
constraintViolations: this.checkConstraintViolations(test, variantResults)
};
}
// Generate test report
generateReport(testId: string): string {
const analysis = this.analyzeTest(testId);
if (!analysis) return 'Test not found';
let report = `# A/B Test Report: ${analysis.testName}\n\n`;
report += `## Status: ${analysis.status}\n\n`;
report += `## Results Summary\n\n`;
report += `| Variant | Sample | Acceptance | Rejection | Bounce | Time (s) |\n`;
report += `|---------|--------|------------|-----------|--------|----------|\n`;
analysis.results.forEach(r => {
report += `| ${r.variantId} | ${r.sampleSize} | ${(r.acceptanceRate * 100).toFixed(1)}% | `;
report += `${(r.rejectionRate * 100).toFixed(1)}% | ${(r.bounceRate * 100).toFixed(1)}% | `;
report += `${r.avgTimeToDecision.toFixed(1)} |\n`;
});
report += `\n## Analysis\n\n`;
report += `**Winner:** ${analysis.winner} with ${analysis.winnerLift} lift\n\n`;
report += `**Can Conclude:** ${analysis.canConclude ? 'Yes' : 'No - need more data'}\n\n`;
report += `**Recommendation:** ${analysis.recommendation}\n\n`;
if (analysis.constraintViolations.length > 0) {
report += `## Constraint Violations\n\n`;
analysis.constraintViolations.forEach(v => {
report += `- ${v}\n`;
});
}
return report;
}
// Private helper methods
private validateTestConfig(config: ABTestConfig): { valid: boolean; errors: string[] } {
const errors: string[] = [];
if (config.variants.length < 2) {
errors.push('Test must have at least 2 variants');
}
const weightSum = config.variants.reduce((sum, v) => sum + v.weight, 0);
if (Math.abs(weightSum - 1) > 0.01) {
errors.push('Variant weights must sum to 1');
}
if (config.traffic <= 0 || config.traffic > 1) {
errors.push('Traffic allocation must be between 0 and 1');
}
return { valid: errors.length === 0, errors };
}
private checkVariantCompliance(
variant: ConsentVariant,
constraints: TestConstraints
): { compliant: boolean; issues: string[] } {
const issues: string[] = [];
// Check equal prominence if required
if (constraints.equalProminence) {
// In a real implementation, would check button sizes, colors, etc.
// This is a simplified check
}
// Check for shame language if prohibited
if (constraints.noShameLanguage && variant.config.copyVariant) {
const shamePatterns = /no thanks|i don't care|miss out|accept risk/i;
if (shamePatterns.test(variant.config.copyVariant)) {
issues.push('Copy contains shame language');
}
}
return { compliant: issues.length === 0, issues };
}
private hashUserId(userId: string): number {
// Simple hash for deterministic variant assignment
let hash = 0;
for (let i = 0; i < userId.length; i++) {
const char = userId.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash % 1000) / 1000;
}
private createEmptyResult(variantId: string): TestResult {
return {
variantId,
sampleSize: 0,
acceptanceRate: 0,
rejectionRate: 0,
bounceRate: 0,
avgTimeToDecision: 0,
customizationRate: 0,
confidenceInterval: [0, 0],
statisticallySignificant: false
};
}
private updateRate(currentRate: number, count: number, value: number): number {
return ((currentRate * (count - 1)) + value) / count;
}
private updateAverage(currentAvg: number, count: number, value: number): number {
return ((currentAvg * (count - 1)) + value) / count;
}
private calculateConfidenceInterval(rate: number, n: number): [number, number] {
if (n === 0) return [0, 0];
const z = 1.96; // 95% confidence
const se = Math.sqrt((rate * (1 - rate)) / n);
return [
Math.max(0, rate - z * se),
Math.min(1, rate + z * se)
];
}
private checkSignificance(testId: string): boolean {
const results = this.testResults.get(testId);
if (!results || results.size < 2) return false;
const values = Array.from(results.values());
const control = values[0];
const treatment = values[1];
if (control.sampleSize < 100 || treatment.sampleSize < 100) return false;
// Chi-square test simplified
const diff = Math.abs(control.acceptanceRate - treatment.acceptanceRate);
const pooledRate = (control.acceptanceRate * control.sampleSize +
treatment.acceptanceRate * treatment.sampleSize) /
(control.sampleSize + treatment.sampleSize);
const se = Math.sqrt(pooledRate * (1 - pooledRate) *
(1/control.sampleSize + 1/treatment.sampleSize));
const zScore = diff / se;
return zScore > 1.96; // p < 0.05
}
private determineTestStatus(
test: ABTestConfig,
results: TestResult[]
): 'running' | 'concluded' | 'needs_attention' {
const hasEnoughSamples = results.every(r => r.sampleSize >= test.minSampleSize);
const hasSignificance = results.some(r => r.statisticallySignificant);
if (hasEnoughSamples && hasSignificance) return 'concluded';
if (!hasEnoughSamples) return 'running';
return 'needs_attention';
}
private generateRecommendation(
test: ABTestConfig,
results: TestResult[],
winner: TestResult,
lift: number
): string {
if (lift < 1) {
return 'No significant winner - consider extending test or testing different variants';
}
if (lift < 5) {
return `Minor improvement (${lift.toFixed(1)}%) - implement winner but continue monitoring`;
}
return `Strong winner with ${lift.toFixed(1)}% lift - recommend implementing ${winner.variantId}`;
}
private checkConstraintViolations(
test: ABTestConfig,
results: TestResult[]
): string[] {
const violations: string[] = [];
results.forEach(r => {
if (r.acceptanceRate < test.constraints.minConsentRate) {
violations.push(`${r.variantId}: Consent rate ${(r.acceptanceRate * 100).toFixed(1)}% below minimum ${(test.constraints.minConsentRate * 100).toFixed(0)}%`);
}
if (r.bounceRate > test.constraints.maxBounceRate) {
violations.push(`${r.variantId}: Bounce rate ${(r.bounceRate * 100).toFixed(1)}% above maximum ${(test.constraints.maxBounceRate * 100).toFixed(0)}%`);
}
});
return violations;
}
}
interface CreateTestResult {
success: boolean;
testId?: string;
errors?: string[];
}
interface VariantAssignment {
testId: string;
variantId: string;
variantName: string;
config: VariantConfig;
}
interface ConsentEvent {
action: 'accept_all' | 'reject_all' | 'customize' | 'bounce';
timeToDecision?: number;
timestamp: number;
}
interface TestAnalysis {
testId: string;
testName: string;
status: 'running' | 'concluded' | 'needs_attention';
results: TestResult[];
winner: string;
winnerLift: string;
recommendation: string;
canConclude: boolean;
constraintViolations: string[];
}
export { ConsentABTestingFramework, ABTestConfig, ConsentVariant };
```
## Mobile-Specific Design Considerations
Mobile users have fundamentally different needs and behaviors:
```typescript
// mobile-consent-optimizer.ts
// Optimize consent UX for mobile devices
interface MobileOptimizationConfig {
device: 'smartphone' | 'tablet';
orientation: 'portrait' | 'landscape';
screenWidth: number;
screenHeight: number;
touchCapable: boolean;
connectionSpeed: 'slow' | 'medium' | 'fast';
}
interface MobileConsentDesign {
placement: string;
buttonSize: { width: string; height: string };
fontSize: string;
spacing: string;
animation: string;
copyLength: 'minimal' | 'standard' | 'full';
gestureSupport: boolean;
}
class MobileConsentOptimizer {
// Mobile research findings
private readonly mobileInsights = {
thumbZonePercentage: 0.75, // 75% of interactions in thumb zone
optimalButtonHeight: 48, // px, minimum touch target
preferredFontSize: 16, // px, prevents zoom on iOS
maxCopyWords: 30, // Mobile users read 50% fewer words
centerModalLift: 0.23, // 23% higher acceptance on mobile
bottomBarDecline: -0.08 // 8% lower acceptance vs desktop
};
// Generate mobile-optimized design
generateOptimalDesign(config: MobileOptimizationConfig): MobileConsentDesign {
const isSmallScreen = config.screenWidth < 375;
const isLandscape = config.orientation === 'landscape';
return {
placement: this.determinePlacement(config),
buttonSize: this.determineButtonSize(config),
fontSize: this.determineFontSize(config),
spacing: this.determineSpacing(config),
animation: this.determineAnimation(config),
copyLength: this.determineCopyLength(config),
gestureSupport: config.touchCapable
};
}
private determinePlacement(config: MobileOptimizationConfig): string {
// Center modal performs 23% better on mobile
if (config.orientation === 'portrait' && config.screenHeight > 600) {
return 'center_modal';
}
// Landscape or small screens: bottom drawer
return 'bottom_drawer';
}
private determineButtonSize(config: MobileOptimizationConfig): { width: string; height: string } {
// Minimum 48px for touch targets (Apple/Google guidelines)
const minHeight = Math.max(48, this.mobileInsights.optimalButtonHeight);
// Full width on small screens
if (config.screenWidth < 375) {
return { width: '100%', height: `${minHeight}px` };
}
// Side by side on larger screens
return { width: '45%', height: `${minHeight}px` };
}
private determineFontSize(config: MobileOptimizationConfig): string {
// 16px minimum prevents iOS zoom
return config.screenWidth < 375 ? '16px' : '15px';
}
private determineSpacing(config: MobileOptimizationConfig): string {
// Larger touch spacing on mobile
return config.screenWidth < 375 ? '16px' : '12px';
}
private determineAnimation(config: MobileOptimizationConfig): string {
// No animation on slow connections
if (config.connectionSpeed === 'slow') {
return 'none';
}
return 'slide-up';
}
private determineCopyLength(config: MobileOptimizationConfig): 'minimal' | 'standard' | 'full' {
// Less copy on mobile
if (config.screenWidth < 375) return 'minimal';
if (config.device === 'smartphone') return 'standard';
return 'full';
}
// Generate mobile-optimized CSS
generateMobileCSS(design: MobileConsentDesign): string {
return `
/* Mobile-optimized consent banner */
.consent-banner-mobile {
position: fixed;
${design.placement === 'center_modal' ?
'top: 50%; left: 50%; transform: translate(-50%, -50%);' :
'bottom: 0; left: 0; right: 0;'}
max-width: 100vw;
padding: 20px;
background: white;
border-radius: ${design.placement === 'center_modal' ? '16px' : '16px 16px 0 0'};
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.15);
z-index: 999999;
-webkit-overflow-scrolling: touch;
}
.consent-banner-mobile .consent-title {
font-size: 18px;
font-weight: 600;
margin: 0 0 12px;
line-height: 1.3;
}
.consent-banner-mobile .consent-description {
font-size: ${design.fontSize};
line-height: 1.5;
margin: 0 0 20px;
color: #666;
}
.consent-banner-mobile .consent-buttons {
display: flex;
flex-direction: ${design.buttonSize.width === '100%' ? 'column' : 'row'};
gap: ${design.spacing};
}
.consent-banner-mobile .consent-btn {
width: ${design.buttonSize.width};
height: ${design.buttonSize.height};
min-height: 48px; /* Touch target minimum */
font-size: 16px; /* Prevent iOS zoom */
font-weight: 500;
border: none;
border-radius: 8px;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
/* Equal prominence for both buttons */
.consent-banner-mobile .consent-btn-accept,
.consent-banner-mobile .consent-btn-reject {
background: #3b82f6;
color: white;
}
.consent-banner-mobile .consent-btn:active {
transform: scale(0.98);
opacity: 0.9;
}
/* Safe area for notched devices */
@supports (padding-bottom: env(safe-area-inset-bottom)) {
.consent-banner-mobile {
padding-bottom: calc(20px + env(safe-area-inset-bottom));
}
}
/* Animation */
${design.animation === 'slide-up' ? `
@keyframes slideUp {
from {
transform: translateY(100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.consent-banner-mobile {
animation: slideUp 0.3s ease-out;
}
` : ''}
/* Reduced motion preference */
@media (prefers-reduced-motion: reduce) {
.consent-banner-mobile {
animation: none;
}
}
`;
}
// Generate mobile-specific copy
generateMobileCopy(length: 'minimal' | 'standard' | 'full'): MobileCopy {
const copyVariants: Record = {
minimal: {
title: 'Cookie Preferences',
description: 'We use cookies to improve your experience.',
acceptText: 'Accept',
rejectText: 'Decline',
wordCount: 8
},
standard: {
title: 'Your Privacy Choices',
description: 'We use cookies to personalize content and analyze traffic. You can customize your preferences below.',
acceptText: 'Accept All',
rejectText: 'Reject All',
wordCount: 20
},
full: {
title: 'Privacy and Cookie Settings',
description: 'We use cookies and similar technologies to provide you with a better experience, personalize content, and analyze site traffic. Choose your preferences or accept all cookies.',
acceptText: 'Accept All Cookies',
rejectText: 'Reject Non-Essential',
wordCount: 35
}
};
return copyVariants[length];
}
}
interface MobileCopy {
title: string;
description: string;
acceptText: string;
rejectText: string;
wordCount: number;
}
export { MobileConsentOptimizer, MobileOptimizationConfig, MobileConsentDesign };
```
## Research Data Summary Tables
### Placement Impact
| Placement | Desktop Acceptance | Mobile Acceptance | Avg Time to Decision | Bounce Rate |
|-----------|-------------------|-------------------|---------------------|-------------|
| Bottom bar | 62% | 54% | 4.2s | 8% |
| Top bar | 58% | 49% | 3.8s | 11% |
| Center modal | 71% | 77% | 2.1s | 6% |
| Corner popup | 65% | 61% | 5.3s | 5% |
| Full screen | 74% | 79% | 1.8s | 12% |
### Button Color Impact
| Accept Color | Reject Color | Acceptance Rate | Notes |
|--------------|--------------|-----------------|-------|
| Green | Gray | 72% | +12% vs neutral |
| Blue | Gray | 68% | Best for B2B/SaaS |
| Brand color | Brand color | 64% | Equal prominence (compliant) |
| Green | Red | 75% | ⚠️ Red reject is manipulative |
| Blue | Blue | 64% | Most compliant approach |
### Timing Impact
| Display Timing | Acceptance Rate | Notes |
|----------------|-----------------|-------|
| Immediate (0s) | 64% | Baseline |
| After 2 seconds | 69% | +5% |
| After scroll/interaction | 72% | +8% |
| On second page view | 78% | +14% (may violate requirements) |
## Frequently Asked Questions
### Is it ethical to optimize consent rates?
Yes, but within strict boundaries. Optimization should focus on clarity, timing, and user experience—not manipulation. You cannot use dark patterns, asymmetric prominence, or shame language. The goal is informed consent, not maximized consent.
### What's the minimum acceptable consent rate?
There's no legal minimum, but rates below 50% often indicate UX issues or high privacy-concern audiences. Rates above 85% might indicate overly aggressive tactics that should be reviewed for compliance.
### How do I know if my A/B test variants are compliant?
Before testing, validate each variant against DSA, CPRA, and GDPR requirements. Key checks: equal button prominence, no pre-checked boxes, no shame language, and symmetric effort for accept/reject paths. The framework above includes compliance validation.
### Should I delay showing the consent banner?
A 2-second delay increases acceptance by ~5%, likely because users have context for what they're consenting to. However, some regulators may view significant delays as attempting to collect data before consent. A 1-2 second delay is generally considered safe.
### How long should consent banner copy be?
On mobile: 20-30 words maximum. On desktop: up to 50 words. Users skim, not read. The most effective copy clearly states what you're asking for and why, in as few words as possible.
## Designing for Trust and Conversion
Effective consent design isn't about tricks—it's about clarity, respect, and user experience. Our research consistently shows that the highest-performing consent interfaces are those that:
1. **Respect user attention** by appearing at the right moment with clear, concise messaging
2. **Provide genuine choice** with equal prominence for all options
3. **Build trust** through transparency about data use
4. **Optimize for mobile** where most interactions now occur
5. **Test systematically** within ethical boundaries
The psychology of consent is not about manipulation—it's about understanding how users make decisions and removing friction from informed choice. When you design for genuine user agency, you build long-term trust that translates to better engagement, lower complaint rates, and sustainable marketing relationships.
Use the research findings, optimization frameworks, and testing tools in this guide to create consent experiences that serve both your users and your business objectives.