Takaisin blogiin
Compliance

Dark Pattern Legislation: Global Crackdown in 2025

Lisa Andersson, Regulatory AffairsOctober 16, 202516 min lukuaika
Dark PatternsLegislationDSAFTC

TLDR: Your A/B test showed the green Accept button with tiny grey Reject link increased consent by 40%. Congratulations—that's now explicitly illegal in the EU and increasingly prosecuted in the US.

Read full summary Survey of global dark pattern legislation including the EU Digital Services Act, US state laws, and regulatory guidance. Learn which interface designs are now prohibited and how to audit your consent flows for compliance. This guide provides detection algorithms, remediation strategies, and production-ready code for building ethical consent interfaces. *Summary by Claude AI*
## The "Growth Hack" That Cost €90 Million In 2023, a major social platform's growth team celebrated. Their redesigned consent flow had increased acceptance rates from 31% to 67%. The new design used a bright blue "Accept" button, a barely-visible "Manage preferences" text link, pre-selected toggles for "personalization," and a confirm-shaming message: "Are you sure? You'll see less relevant content." The celebration lasted until the Irish Data Protection Commission's investigation. The fine: €390 million. Amazon's cookie banner got them €35 million from France. Google's cost €150 million. These weren't edge cases—they were standard industry practices that became explicit violations. The era of optimizing consent rates through manipulation is over. What worked in 2020 is illegal in 2025. ## Is there new legislation against dark patterns in 2025? Yes, 2025 marks a global crackdown on dark patterns. The EU's **Digital Services Act (DSA)** directly prohibits deceptive patterns, while the US is seeing increased enforcement by the FTC and state Attorneys General under existing consumer protection laws, specifically targeting manipulative cookie banners and subscription flows. This comprehensive guide covers the legal landscape and provides technical implementations for compliant, ethical design. ## Introduction The era of manipulative interface design is ending. What began as controversial UX practices are now explicitly illegal across major jurisdictions, with regulators actively investigating and fining companies that deploy dark patterns in their consent interfaces. For engineering teams, this shift requires a fundamental rethinking of how consent flows are designed. The "optimization for consent rates at any cost" mindset must be replaced with "optimization within ethical constraints." This isn't just about avoiding fines—dark patterns erode user trust and, increasingly, face public backlash when exposed. This guide provides a comprehensive overview of dark pattern legislation, technical detection methods, and concrete implementations for building consent interfaces that are both compliant and effective. ## Global Dark Pattern Legislation Overview ### European Union - Digital Services Act (DSA) The DSA represents the most comprehensive legislative action against dark patterns, explicitly prohibiting deceptive interface designs. ```typescript // dsa-dark-pattern-compliance.ts // EU Digital Services Act dark pattern compliance checker interface DSAViolation { type: DSADarkPatternType; element: string; description: string; article: string; severity: 'low' | 'medium' | 'high' | 'critical'; remediation: string; } type DSADarkPatternType = | 'visual_prominence_asymmetry' | 'confirm_shaming' | 'hidden_information' | 'nagging' | 'obstruction' | 'forced_continuity' | 'default_settings' | 'emotional_steering' | 'trick_questions' | 'fake_urgency'; interface DSAComplianceConfig { strictMode: boolean; reportViolations: boolean; blockNonCompliant: boolean; } class DSADarkPatternChecker { private violations: DSAViolation[] = []; private config: DSAComplianceConfig; // DSA Article 25 prohibited practices private readonly prohibitedPractices = { visual_prominence_asymmetry: { article: 'DSA Article 25(1)(a)', description: 'Design choices that give more visual prominence to certain choices', severity: 'high' as const, detection: 'Button size, color contrast, positioning differences' }, confirm_shaming: { article: 'DSA Article 25(1)(b)', description: 'Language designed to make users feel guilty for declining', severity: 'medium' as const, detection: 'Emotional manipulation in rejection text' }, hidden_information: { article: 'DSA Article 25(1)(c)', description: 'Making important options difficult to find or understand', severity: 'high' as const, detection: 'Reject/decline options hidden or obscured' }, nagging: { article: 'DSA Article 25(1)(d)', description: 'Repeatedly asking users to reconsider after they decline', severity: 'medium' as const, detection: 'Repeated consent prompts after rejection' }, obstruction: { article: 'DSA Article 25(1)(e)', description: 'Making declining harder than accepting', severity: 'critical' as const, detection: 'Asymmetric effort required for different choices' }, default_settings: { article: 'DSA Article 25(2)', description: 'Pre-selecting privacy-invasive options', severity: 'critical' as const, detection: 'Checkboxes pre-checked by default' } }; constructor(config: DSAComplianceConfig) { this.config = config; } // Analyze consent interface for DSA violations analyzeInterface(elements: ConsentInterfaceElements): DSAComplianceReport { this.violations = []; // Check visual prominence asymmetry this.checkVisualProminence(elements); // Check for confirm-shaming language this.checkConfirmShaming(elements); // Check for hidden information this.checkHiddenInformation(elements); // Check default settings this.checkDefaultSettings(elements); // Check obstruction patterns this.checkObstruction(elements); return this.generateReport(); } private checkVisualProminence(elements: ConsentInterfaceElements): void { const { acceptButton, rejectButton } = elements; if (!acceptButton || !rejectButton) { this.addViolation({ type: 'visual_prominence_asymmetry', element: 'buttons', description: 'Missing accept or reject button - one choice is hidden', article: 'DSA Article 25(1)(a)', severity: 'critical', remediation: 'Provide clearly visible buttons for both accepting and rejecting' }); return; } // Check button size asymmetry const sizeDifference = Math.abs( (acceptButton.width * acceptButton.height) - (rejectButton.width * rejectButton.height) ); const averageSize = (acceptButton.width * acceptButton.height + rejectButton.width * rejectButton.height) / 2; const sizeAsymmetry = sizeDifference / averageSize; if (sizeAsymmetry > 0.3) { // More than 30% size difference this.addViolation({ type: 'visual_prominence_asymmetry', element: 'button_size', description: `Button size asymmetry: ${(sizeAsymmetry * 100).toFixed(0)}% difference`, article: 'DSA Article 25(1)(a)', severity: sizeAsymmetry > 0.5 ? 'critical' : 'high', remediation: 'Make accept and reject buttons similar in size' }); } // Check color contrast asymmetry if (acceptButton.backgroundColor && rejectButton.backgroundColor) { const acceptContrast = this.calculateContrastRatio( acceptButton.backgroundColor, acceptButton.parentBackground || '#ffffff' ); const rejectContrast = this.calculateContrastRatio( rejectButton.backgroundColor, rejectButton.parentBackground || '#ffffff' ); const contrastDifference = Math.abs(acceptContrast - rejectContrast); if (contrastDifference > 2) { this.addViolation({ type: 'visual_prominence_asymmetry', element: 'button_contrast', description: 'Significant contrast difference between buttons', article: 'DSA Article 25(1)(a)', severity: 'high', remediation: 'Use similar contrast levels for both buttons' }); } } // Check if reject is styled as a link vs button if (acceptButton.type === 'button' && rejectButton.type === 'link') { this.addViolation({ type: 'visual_prominence_asymmetry', element: 'button_style', description: 'Accept is a button while reject is styled as a link', article: 'DSA Article 25(1)(a)', severity: 'high', remediation: 'Style both options as buttons with equal prominence' }); } } private checkConfirmShaming(elements: ConsentInterfaceElements): void { const shamingPatterns = [ { pattern: /no,?\s*(thanks|i\s+don'?t|i\s+prefer)/i, severity: 'low' as const }, { pattern: /i\s+don'?t\s+care/i, severity: 'medium' as const }, { pattern: /i\s+accept\s+risk/i, severity: 'high' as const }, { pattern: /i\s+don'?t\s+want\s+(to\s+save|discounts|benefits)/i, severity: 'high' as const }, { pattern: /no,?\s*i\s+(hate|dislike)/i, severity: 'high' as const }, { pattern: /miss\s+out/i, severity: 'medium' as const }, { pattern: /lose\s+(access|benefits)/i, severity: 'high' as const }, { pattern: /continue\s+without/i, severity: 'low' as const }, { pattern: /not\s+interested\s+in\s+(saving|improving)/i, severity: 'medium' as const } ]; const allText = [ elements.rejectButton?.text, elements.rejectLink?.text, elements.dismissText ].filter(Boolean).join(' '); shamingPatterns.forEach(({ pattern, severity }) => { if (pattern.test(allText)) { this.addViolation({ type: 'confirm_shaming', element: 'reject_text', description: `Confirm-shaming language detected: matches "${pattern.source}"`, article: 'DSA Article 25(1)(b)', severity, remediation: 'Use neutral language like "Decline" or "No thanks"' }); } }); } private checkHiddenInformation(elements: ConsentInterfaceElements): void { // Check if reject option is hidden in settings/preferences if (elements.rejectRequiresNavigation) { this.addViolation({ type: 'hidden_information', element: 'reject_location', description: 'Reject option requires navigating to another screen', article: 'DSA Article 25(1)(c)', severity: 'critical', remediation: 'Provide reject option on the initial consent screen' }); } // Check if reject is below the fold if (elements.rejectButton?.belowFold || elements.rejectLink?.belowFold) { this.addViolation({ type: 'hidden_information', element: 'reject_visibility', description: 'Reject option is below the visible fold', article: 'DSA Article 25(1)(c)', severity: 'high', remediation: 'Ensure reject option is visible without scrolling' }); } // Check font size difference if (elements.acceptButton?.fontSize && elements.rejectButton?.fontSize) { const fontDiff = elements.acceptButton.fontSize - elements.rejectButton.fontSize; if (fontDiff > 2) { // More than 2px difference this.addViolation({ type: 'hidden_information', element: 'font_size', description: `Reject text is ${fontDiff}px smaller than accept text`, article: 'DSA Article 25(1)(c)', severity: 'medium', remediation: 'Use similar font sizes for both options' }); } } } private checkDefaultSettings(elements: ConsentInterfaceElements): void { if (elements.checkboxes) { elements.checkboxes.forEach(checkbox => { if (checkbox.defaultChecked && !checkbox.isNecessary) { this.addViolation({ type: 'default_settings', element: `checkbox_${checkbox.id}`, description: `Non-essential checkbox "${checkbox.label}" is pre-checked`, article: 'DSA Article 25(2)', severity: 'critical', remediation: 'Do not pre-check optional consent checkboxes' }); } }); } // Check if "Accept All" is the default/highlighted option if (elements.acceptButton?.isDefault && !elements.rejectButton?.isDefault) { this.addViolation({ type: 'default_settings', element: 'default_action', description: 'Accept is marked as default action (e.g., autofocus)', article: 'DSA Article 25(2)', severity: 'medium', remediation: 'Do not set either option as default' }); } } private checkObstruction(elements: ConsentInterfaceElements): void { // Check click count asymmetry if (elements.acceptClickCount !== undefined && elements.rejectClickCount !== undefined) { if (elements.rejectClickCount > elements.acceptClickCount) { this.addViolation({ type: 'obstruction', element: 'click_count', description: `Rejecting requires ${elements.rejectClickCount} clicks vs ${elements.acceptClickCount} for accepting`, article: 'DSA Article 25(1)(e)', severity: 'critical', remediation: 'Require equal steps for both choices' }); } } // Check if rejection shows additional warnings/confirmations if (elements.rejectShowsWarning) { this.addViolation({ type: 'obstruction', element: 'rejection_warning', description: 'Rejection triggers additional warning dialog', article: 'DSA Article 25(1)(e)', severity: 'high', remediation: 'Do not add extra steps to the rejection flow' }); } // Check if "Accept All" is the only visible one-click option if (elements.hasAcceptAllOneClick && !elements.hasRejectAllOneClick) { this.addViolation({ type: 'obstruction', element: 'one_click_asymmetry', description: 'Accept All is one click, but Reject All requires multiple steps', article: 'DSA Article 25(1)(e)', severity: 'critical', remediation: 'Provide Reject All with equal prominence and ease of use' }); } } private calculateContrastRatio(color1: string, color2: string): number { // Simplified contrast calculation const getLuminance = (hex: string): number => { const rgb = parseInt(hex.replace('#', ''), 16); const r = (rgb >> 16) & 0xff; const g = (rgb >> 8) & 0xff; const b = (rgb >> 0) & 0xff; return 0.299 * r + 0.587 * g + 0.114 * b; }; const l1 = getLuminance(color1); const l2 = getLuminance(color2); const lighter = Math.max(l1, l2); const darker = Math.min(l1, l2); return (lighter + 0.05) / (darker + 0.05); } private addViolation(violation: DSAViolation): void { this.violations.push(violation); } private generateReport(): DSAComplianceReport { const criticalCount = this.violations.filter(v => v.severity === 'critical').length; const highCount = this.violations.filter(v => v.severity === 'high').length; return { compliant: this.violations.length === 0, violations: this.violations, summary: { total: this.violations.length, critical: criticalCount, high: highCount, medium: this.violations.filter(v => v.severity === 'medium').length, low: this.violations.filter(v => v.severity === 'low').length }, riskLevel: criticalCount > 0 ? 'critical' : highCount > 0 ? 'high' : this.violations.length > 0 ? 'medium' : 'low', recommendations: this.generateRecommendations() }; } private generateRecommendations(): string[] { const recommendations: string[] = []; const byType = new Map(); this.violations.forEach(v => { const existing = byType.get(v.type) || []; existing.push(v); byType.set(v.type, existing); }); if (byType.has('obstruction')) { recommendations.push( 'CRITICAL: Implement "Reject All" button with equal prominence to "Accept All"' ); } if (byType.has('default_settings')) { recommendations.push( 'CRITICAL: Remove pre-checked boxes for non-essential cookies/purposes' ); } if (byType.has('visual_prominence_asymmetry')) { recommendations.push( 'HIGH: Redesign buttons to have equal visual weight (size, color, contrast)' ); } if (byType.has('confirm_shaming')) { recommendations.push( 'Replace emotional or guilt-inducing language with neutral alternatives' ); } if (byType.has('hidden_information')) { recommendations.push( 'Ensure all options are visible and accessible on the initial screen' ); } return recommendations; } } interface ConsentInterfaceElements { acceptButton?: ButtonElement; rejectButton?: ButtonElement; rejectLink?: LinkElement; dismissText?: string; checkboxes?: CheckboxElement[]; rejectRequiresNavigation?: boolean; rejectShowsWarning?: boolean; acceptClickCount?: number; rejectClickCount?: number; hasAcceptAllOneClick?: boolean; hasRejectAllOneClick?: boolean; } interface ButtonElement { text?: string; width: number; height: number; backgroundColor?: string; parentBackground?: string; type: 'button' | 'link'; belowFold?: boolean; fontSize?: number; isDefault?: boolean; } interface LinkElement { text?: string; belowFold?: boolean; } interface CheckboxElement { id: string; label: string; defaultChecked: boolean; isNecessary: boolean; } interface DSAComplianceReport { compliant: boolean; violations: DSAViolation[]; summary: { total: number; critical: number; high: number; medium: number; low: number; }; riskLevel: 'low' | 'medium' | 'high' | 'critical'; recommendations: string[]; } export { DSADarkPatternChecker, DSAViolation, DSAComplianceReport }; ``` ### United States - FTC and State Laws The FTC has intensified enforcement against dark patterns using existing Section 5 authority, while states like California have explicit dark pattern prohibitions. ```typescript // us-dark-pattern-compliance.ts // US dark pattern compliance (FTC + State laws) interface USRegulatoryFramework { federal: { authority: 'FTC Section 5'; standard: 'Unfair or deceptive acts or practices'; enforcement: 'Civil penalties up to $50,000 per violation'; }; states: { california: { law: 'CPRA'; section: '1798.140(l)'; prohibitions: string[]; }; colorado: { law: 'CPA'; section: '6-1-1303(16)'; prohibitions: string[]; }; connecticut: { law: 'CTDPA'; section: '42-520'; prohibitions: string[]; }; }; } interface FTCViolation { type: FTCDarkPatternType; description: string; evidence: string; potentialPenalty: string; precedent?: string; } type FTCDarkPatternType = | 'hidden_costs' | 'bait_and_switch' | 'trick_questions' | 'forced_continuity' | 'roach_motel' | 'privacy_zuckering' | 'misdirection' | 'hidden_subscription'; class USFTCComplianceChecker { private violations: FTCViolation[] = []; // FTC enforcement precedents private readonly enforcementPrecedents = { vonage_2022: { company: 'Vonage', year: 2022, violation: 'Making it difficult to cancel subscriptions', penalty: '$100 million' }, epic_games_2022: { company: 'Epic Games', year: 2022, violation: 'Dark patterns targeting children', penalty: '$520 million' }, amazon_2023: { company: 'Amazon', year: 2023, violation: 'Difficult Prime cancellation (alleged)', status: 'Ongoing litigation' } }; analyzeForFTCCompliance(flow: ConsentFlow): FTCComplianceReport { this.violations = []; // Check for "roach motel" pattern (easy in, hard out) this.checkRoachMotel(flow); // Check for forced continuity this.checkForcedContinuity(flow); // Check for hidden information this.checkHiddenInformation(flow); // Check for misdirection this.checkMisdirection(flow); // Check for privacy zuckering this.checkPrivacyZuckering(flow); return this.generateReport(); } private checkRoachMotel(flow: ConsentFlow): void { // Compare steps to opt-in vs opt-out if (flow.optOutSteps > flow.optInSteps) { this.violations.push({ type: 'roach_motel', description: 'Opting out requires more steps than opting in', evidence: `Opt-in: ${flow.optInSteps} steps, Opt-out: ${flow.optOutSteps} steps`, potentialPenalty: 'Up to $50,000 per affected consumer', precedent: 'FTC v. Vonage (2022) - $100M settlement for similar practices' }); } // Check if preference center is hard to find if (flow.preferenceCenterDepth > 2) { this.violations.push({ type: 'roach_motel', description: 'Preference center buried too deep in navigation', evidence: `${flow.preferenceCenterDepth} clicks required to access preferences`, potentialPenalty: 'Potential injunctive relief and civil penalties' }); } } private checkForcedContinuity(flow: ConsentFlow): void { // Check if consent auto-renews without clear disclosure if (flow.hasAutoRenewal && !flow.autoRenewalClearlyDisclosed) { this.violations.push({ type: 'forced_continuity', description: 'Consent auto-renewal without clear disclosure', evidence: 'Auto-renewal enabled without prominent notice', potentialPenalty: 'ROSCA violations up to $50,120 per violation' }); } } private checkHiddenInformation(flow: ConsentFlow): void { // Check for critical info in fine print if (flow.importantInfoInFineprint) { this.violations.push({ type: 'misdirection', description: 'Important consent information hidden in fine print', evidence: 'Key data usage information requires scrolling or expanding', potentialPenalty: 'Section 5 deception finding' }); } // Check if data sharing partners are clearly listed if (flow.dataPartnerCount > 0 && !flow.partnersListVisible) { this.violations.push({ type: 'privacy_zuckering', description: 'Data sharing partners not prominently disclosed', evidence: `${flow.dataPartnerCount} partners not visible in main consent UI`, potentialPenalty: 'Potential privacy-related FTC action' }); } } private checkMisdirection(flow: ConsentFlow): void { // Check for misleading button labels const misleadingPatterns = [ { pattern: /accept.*to\s+continue/i, issue: 'Implies consent is required' }, { pattern: /agree.*terms/i, issue: 'Bundles consent with terms acceptance' }, { pattern: /ok|okay|got\s+it/i, issue: 'Ambiguous language for consent' } ]; misleadingPatterns.forEach(({ pattern, issue }) => { if (flow.buttonLabels?.some(label => pattern.test(label))) { this.violations.push({ type: 'misdirection', description: `Misleading button label: ${issue}`, evidence: `Button text matches pattern: ${pattern.source}`, potentialPenalty: 'Section 5 deception under "clear and conspicuous" standard' }); } }); } private checkPrivacyZuckering(flow: ConsentFlow): void { // Check if privacy-intrusive options are presented as beneficial if (flow.presentsTrackingAsBenefit) { this.violations.push({ type: 'privacy_zuckering', description: 'Privacy-invasive options presented misleadingly as user benefits', evidence: 'Tracking framed as "personalization" without disclosure of data use', potentialPenalty: 'Section 5 unfairness or deception' }); } // Check for confusing privacy settings if (flow.privacySettingsInverted) { this.violations.push({ type: 'trick_questions', description: 'Privacy settings use inverted logic (on = less privacy)', evidence: 'Toggle "on" increases data collection instead of protecting privacy', potentialPenalty: 'Section 5 deception - confusing interface design' }); } } private generateReport(): FTCComplianceReport { const hasViolations = this.violations.length > 0; const maxPenalty = hasViolations ? this.estimateMaxPenalty() : '$0'; return { compliant: !hasViolations, violations: this.violations, riskAssessment: { violationCount: this.violations.length, estimatedMaxPenalty: maxPenalty, litigationRisk: this.assessLitigationRisk(), reputationalRisk: hasViolations ? 'high' : 'low' }, recommendations: this.generateRecommendations(), relevantPrecedents: this.getRelevantPrecedents() }; } private estimateMaxPenalty(): string { // Rough estimate based on violation types const hasRoachMotel = this.violations.some(v => v.type === 'roach_motel'); const hasForcedContinuity = this.violations.some(v => v.type === 'forced_continuity'); if (hasRoachMotel && hasForcedContinuity) { return 'Potential eight-figure settlement based on Vonage precedent'; } else if (hasRoachMotel || hasForcedContinuity) { return 'Potential multi-million dollar settlement'; } else { return 'Potential six-figure penalty per violation'; } } private assessLitigationRisk(): 'low' | 'medium' | 'high' | 'very_high' { const severityScore = this.violations.reduce((score, v) => { if (v.type === 'roach_motel' || v.type === 'forced_continuity') return score + 3; if (v.type === 'privacy_zuckering' || v.type === 'misdirection') return score + 2; return score + 1; }, 0); if (severityScore >= 6) return 'very_high'; if (severityScore >= 4) return 'high'; if (severityScore >= 2) return 'medium'; return 'low'; } private generateRecommendations(): string[] { const recommendations: string[] = []; if (this.violations.some(v => v.type === 'roach_motel')) { recommendations.push( 'Implement symmetrical opt-in/opt-out processes with equal number of steps' ); recommendations.push( 'Make preference center accessible within 2 clicks from any page' ); } if (this.violations.some(v => v.type === 'misdirection')) { recommendations.push( 'Use clear, unambiguous button labels: "Accept" and "Decline"' ); recommendations.push( 'Separate consent from terms of service acceptance' ); } if (this.violations.some(v => v.type === 'privacy_zuckering')) { recommendations.push( 'Clearly disclose all data sharing partners in the main consent UI' ); recommendations.push( 'Frame data collection accurately without misleading "benefit" language' ); } return recommendations; } private getRelevantPrecedents(): typeof this.enforcementPrecedents { return this.enforcementPrecedents; } } interface ConsentFlow { optInSteps: number; optOutSteps: number; preferenceCenterDepth: number; hasAutoRenewal: boolean; autoRenewalClearlyDisclosed: boolean; importantInfoInFineprint: boolean; dataPartnerCount: number; partnersListVisible: boolean; buttonLabels?: string[]; presentsTrackingAsBenefit: boolean; privacySettingsInverted: boolean; } interface FTCComplianceReport { compliant: boolean; violations: FTCViolation[]; riskAssessment: { violationCount: number; estimatedMaxPenalty: string; litigationRisk: 'low' | 'medium' | 'high' | 'very_high'; reputationalRisk: 'low' | 'medium' | 'high'; }; recommendations: string[]; relevantPrecedents: any; } export { USFTCComplianceChecker, FTCViolation, FTCComplianceReport }; ``` ## Automated Dark Pattern Detection Building automated detection systems helps catch dark patterns before they reach users: ```typescript // dark-pattern-detector.ts // Automated dark pattern detection system interface DetectionResult { patternType: string; confidence: number; // 0-1 element: HTMLElement | null; evidence: string; screenshot?: string; } interface DetectionConfig { strictMode: boolean; captureScreenshots: boolean; reportToServer: boolean; blockIfDetected: boolean; } class DarkPatternDetector { private config: DetectionConfig; private detectedPatterns: DetectionResult[] = []; constructor(config: DetectionConfig) { this.config = config; } // Run all detection checks on the current page async detectPatterns(): Promise { this.detectedPatterns = []; // Visual analysis await this.detectVisualAsymmetry(); await this.detectHiddenElements(); // Text analysis await this.detectConfirmShaming(); await this.detectMisleadingLabels(); // Behavioral analysis await this.detectClickJacking(); await this.detectForcedAction(); // Interaction analysis await this.detectNagging(); return this.generateReport(); } // Detect visual prominence asymmetry private async detectVisualAsymmetry(): Promise { const consentBanner = document.querySelector('[class*="consent"], [class*="cookie"], [id*="consent"], [id*="cookie"]'); if (!consentBanner) return; const buttons = consentBanner.querySelectorAll('button, [role="button"], a.btn, a.button'); if (buttons.length < 2) return; const buttonMetrics: { element: Element; area: number; contrast: number }[] = []; buttons.forEach(button => { const rect = button.getBoundingClientRect(); const styles = window.getComputedStyle(button); buttonMetrics.push({ element: button, area: rect.width * rect.height, contrast: this.estimateContrast(styles.backgroundColor, styles.color) }); }); // Sort by area buttonMetrics.sort((a, b) => b.area - a.area); // Check if largest button is "accept" and much larger than others if (buttonMetrics.length >= 2) { const largest = buttonMetrics[0]; const secondLargest = buttonMetrics[1]; const areaRatio = largest.area / secondLargest.area; if (areaRatio > 1.5) { const largestText = largest.element.textContent?.toLowerCase() || ''; if (largestText.includes('accept') || largestText.includes('agree') || largestText.includes('allow')) { this.detectedPatterns.push({ patternType: 'visual_asymmetry', confidence: Math.min((areaRatio - 1) / 2, 1), element: largest.element as HTMLElement, evidence: `Accept button is ${areaRatio.toFixed(1)}x larger than alternatives` }); } } } } // Detect hidden or obscured elements private async detectHiddenElements(): Promise { const rejectPatterns = [ 'reject', 'decline', 'refuse', 'deny', 'no thanks', 'manage', 'settings', 'preferences', 'customize' ]; const consentArea = document.querySelector('[class*="consent"], [class*="cookie"]'); if (!consentArea) return; const allText = consentArea.querySelectorAll('button, a, span, p'); allText.forEach(element => { const text = element.textContent?.toLowerCase() || ''; const styles = window.getComputedStyle(element); const isRejectOption = rejectPatterns.some(p => text.includes(p)); if (!isRejectOption) return; // Check if element is visually hidden const opacity = parseFloat(styles.opacity); const fontSize = parseFloat(styles.fontSize); const color = styles.color; if (opacity < 0.5) { this.detectedPatterns.push({ patternType: 'hidden_element', confidence: 1 - opacity, element: element as HTMLElement, evidence: `Reject option has low opacity: ${opacity}` }); } if (fontSize < 10) { this.detectedPatterns.push({ patternType: 'hidden_element', confidence: 0.8, element: element as HTMLElement, evidence: `Reject option has very small font: ${fontSize}px` }); } // Check if light gray text on white background if (this.isLowContrastText(color, styles.backgroundColor)) { this.detectedPatterns.push({ patternType: 'hidden_element', confidence: 0.7, element: element as HTMLElement, evidence: 'Reject option has low contrast text' }); } }); } // Detect confirm-shaming language private async detectConfirmShaming(): Promise { const shamingPatterns = [ { regex: /no,?\s+i\s+(hate|don't\s+want|refuse)/i, severity: 0.9 }, { regex: /i\s+prefer\s+(not\s+to\s+save|worse)/i, severity: 0.8 }, { regex: /i\s+don't\s+care\s+about/i, severity: 0.8 }, { regex: /continue\s+without\s+(saving|benefits)/i, severity: 0.6 }, { regex: /miss\s+out\s+on/i, severity: 0.7 }, { regex: /i\s+accept\s+(the\s+)?risk/i, severity: 0.9 }, { regex: /not\s+interested\s+in\s+(saving|improving)/i, severity: 0.7 } ]; const consentArea = document.querySelector('[class*="consent"], [class*="cookie"]'); if (!consentArea) return; const allText = consentArea.textContent || ''; shamingPatterns.forEach(({ regex, severity }) => { const match = allText.match(regex); if (match) { this.detectedPatterns.push({ patternType: 'confirm_shaming', confidence: severity, element: null, evidence: `Shame language detected: "${match[0]}"` }); } }); } // Detect misleading button labels private async detectMisleadingLabels(): Promise { const misleadingPatterns = [ { regex: /^(ok|okay|got\s+it)$/i, issue: 'Ambiguous consent language' }, { regex: /accept.*continue/i, issue: 'Implies consent is required to continue' }, { regex: /^(yes|sure)$/i, issue: 'Informal language obscures consent meaning' } ]; const buttons = document.querySelectorAll('button, [role="button"]'); buttons.forEach(button => { const text = button.textContent?.trim() || ''; misleadingPatterns.forEach(({ regex, issue }) => { if (regex.test(text)) { this.detectedPatterns.push({ patternType: 'misleading_label', confidence: 0.7, element: button as HTMLElement, evidence: `${issue}: "${text}"` }); } }); }); } // Detect click-jacking attempts private async detectClickJacking(): Promise { const consentArea = document.querySelector('[class*="consent"], [class*="cookie"]'); if (!consentArea) return; // Check for invisible overlays const overlays = document.querySelectorAll('[style*="position: absolute"], [style*="position: fixed"]'); overlays.forEach(overlay => { const styles = window.getComputedStyle(overlay); const rect = overlay.getBoundingClientRect(); const consentRect = consentArea.getBoundingClientRect(); // Check if overlay is over consent area but invisible const overlapsConsent = !( rect.right < consentRect.left || rect.left > consentRect.right || rect.bottom < consentRect.top || rect.top > consentRect.bottom ); if (overlapsConsent) { const opacity = parseFloat(styles.opacity); if (opacity < 0.1 && rect.width > 50 && rect.height > 50) { this.detectedPatterns.push({ patternType: 'click_jacking', confidence: 0.9, element: overlay as HTMLElement, evidence: 'Invisible overlay detected over consent area' }); } } }); } // Detect forced action patterns private async detectForcedAction(): Promise { const consentArea = document.querySelector('[class*="consent"], [class*="cookie"]'); if (!consentArea) return; // Check if page content is blocked until consent const pageContent = document.querySelector('main, article, .content, #content'); if (pageContent) { const styles = window.getComputedStyle(pageContent); if (styles.filter.includes('blur') || parseFloat(styles.opacity) < 0.5) { // This might be legitimate, but flag for review this.detectedPatterns.push({ patternType: 'forced_action', confidence: 0.5, element: pageContent as HTMLElement, evidence: 'Page content appears blocked/blurred until consent' }); } } // Check for cookie walls (no option to reject) const hasReject = consentArea.textContent?.toLowerCase().includes('reject') || consentArea.textContent?.toLowerCase().includes('decline') || consentArea.textContent?.toLowerCase().includes('refuse'); if (!hasReject) { this.detectedPatterns.push({ patternType: 'forced_action', confidence: 0.8, element: consentArea as HTMLElement, evidence: 'No visible reject/decline option found' }); } } // Detect nagging patterns private async detectNagging(): Promise { // Check localStorage for previous consent rejections const consentKey = this.findConsentStorageKey(); if (!consentKey) return; const storedConsent = localStorage.getItem(consentKey); if (storedConsent) { try { const consent = JSON.parse(storedConsent); // If user previously rejected but banner is showing again... if (consent.rejected && document.querySelector('[class*="consent"], [class*="cookie"]')) { this.detectedPatterns.push({ patternType: 'nagging', confidence: 0.9, element: null, evidence: 'Consent banner shown again after user previously rejected' }); } } catch { // Ignore parse errors } } } private findConsentStorageKey(): string | null { const possibleKeys = ['consent', 'cookie_consent', 'gdpr_consent', 'privacy_consent']; for (const key of possibleKeys) { if (localStorage.getItem(key)) return key; } return null; } private estimateContrast(bg: string, fg: string): number { // Simplified contrast estimation return 1; // Placeholder - would implement proper WCAG contrast calculation } private isLowContrastText(color: string, background: string): boolean { // Check if text color is too similar to background // Simplified check return color === background || color.includes('rgba(0, 0, 0, 0'); } private generateReport(): DetectionReport { const byType = new Map(); this.detectedPatterns.forEach(p => { const existing = byType.get(p.patternType) || []; existing.push(p); byType.set(p.patternType, existing); }); const highConfidence = this.detectedPatterns.filter(p => p.confidence >= 0.7); return { timestamp: new Date().toISOString(), url: window.location.href, patternsDetected: this.detectedPatterns, summary: { total: this.detectedPatterns.length, highConfidence: highConfidence.length, byType: Object.fromEntries(byType) }, riskLevel: highConfidence.length >= 3 ? 'critical' : highConfidence.length >= 1 ? 'high' : this.detectedPatterns.length >= 1 ? 'medium' : 'low', shouldBlock: this.config.blockIfDetected && highConfidence.length > 0 }; } } interface DetectionReport { timestamp: string; url: string; patternsDetected: DetectionResult[]; summary: { total: number; highConfidence: number; byType: Record; }; riskLevel: 'low' | 'medium' | 'high' | 'critical'; shouldBlock: boolean; } export { DarkPatternDetector, DetectionResult, DetectionReport }; ``` ## Building Ethical Consent Interfaces Here's how to build consent interfaces that are both compliant and effective: ```typescript // ethical-consent-builder.ts // Build compliant, ethical consent interfaces interface EthicalConsentConfig { domain: string; purposes: ConsentPurpose[]; styling: ConsentStyling; copy: ConsentCopy; behavior: ConsentBehavior; } interface ConsentPurpose { id: string; name: string; description: string; necessary: boolean; defaultEnabled: boolean; // Should be false for non-necessary } interface ConsentStyling { accentColor: string; buttonStyle: 'filled' | 'outlined' | 'equal'; // 'equal' recommended position: 'bottom' | 'center' | 'top'; theme: 'light' | 'dark' | 'auto'; } interface ConsentCopy { title: string; description: string; acceptAllText: string; rejectAllText: string; customizeText: string; saveText: string; } interface ConsentBehavior { rememberRejection: boolean; rejectionDurationDays: number; showOnEveryVisit: boolean; enableGranularControl: boolean; } class EthicalConsentBuilder { private config: EthicalConsentConfig; constructor(config: EthicalConsentConfig) { this.validateConfig(config); this.config = config; } // Validate config doesn't contain dark patterns private validateConfig(config: EthicalConsentConfig): void { // Check for pre-enabled non-necessary purposes config.purposes.forEach(purpose => { if (!purpose.necessary && purpose.defaultEnabled) { throw new Error( `Dark pattern detected: Non-necessary purpose "${purpose.id}" is defaultEnabled. ` + `This violates GDPR and DSA requirements.` ); } }); // Check button styling if (config.styling.buttonStyle !== 'equal') { console.warn( '[Consent] Consider using buttonStyle: "equal" to ensure compliance with DSA Article 25' ); } // Check copy for shame language const shamingPatterns = /no thanks|i don't care|miss out|accept risk/i; if (shamingPatterns.test(config.copy.rejectAllText)) { throw new Error( `Dark pattern detected: Reject button text "${config.copy.rejectAllText}" ` + `contains confirm-shaming language.` ); } } // Generate compliant banner HTML generateBannerHTML(): string { const { copy, styling } = this.config; return ` `; } // Generate compliant CSS generateCSS(): string { const { styling } = this.config; return ` .ethical-consent-banner { position: fixed; ${styling.position === 'bottom' ? 'bottom: 0;' : styling.position === 'top' ? 'top: 0;' : 'top: 50%; left: 50%; transform: translate(-50%, -50%);'} left: 0; right: 0; background: ${styling.theme === 'dark' ? '#1a1a1a' : '#ffffff'}; color: ${styling.theme === 'dark' ? '#ffffff' : '#1a1a1a'}; padding: 24px; box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.15); z-index: 999999; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } .consent-content { max-width: 800px; margin: 0 auto; } .consent-content h2 { margin: 0 0 12px; font-size: 18px; font-weight: 600; } .consent-content p { margin: 0 0 20px; font-size: 14px; line-height: 1.5; color: ${styling.theme === 'dark' ? '#b0b0b0' : '#666666'}; } .consent-buttons { display: flex; flex-wrap: wrap; gap: 12px; } /* CRITICAL: Equal prominence for accept and reject */ .consent-btn { padding: 12px 24px; font-size: 14px; font-weight: 500; border-radius: 6px; cursor: pointer; transition: all 0.2s; border: none; } /* Both primary buttons have IDENTICAL styling */ .consent-btn-primary { background: ${styling.accentColor}; color: #ffffff; min-width: 120px; } .consent-btn-primary:hover { opacity: 0.9; } /* Secondary button is visually distinct but accessible */ .consent-btn-secondary { background: transparent; color: ${styling.theme === 'dark' ? '#ffffff' : '#1a1a1a'}; border: 1px solid ${styling.theme === 'dark' ? '#444' : '#ddd'}; } .consent-btn-secondary:hover { background: ${styling.theme === 'dark' ? '#333' : '#f5f5f5'}; } /* Focus states for accessibility */ .consent-btn:focus { outline: 2px solid ${styling.accentColor}; outline-offset: 2px; } /* Mobile responsive */ @media (max-width: 600px) { .consent-buttons { flex-direction: column; } .consent-btn { width: 100%; } } `; } // Generate JavaScript handler generateJavaScript(): string { return ` class EthicalConsentManager { constructor() { this.banner = document.querySelector('.ethical-consent-banner'); this.purposes = ${JSON.stringify(this.config.purposes)}; this.behavior = ${JSON.stringify(this.config.behavior)}; this.bindEvents(); this.checkExistingConsent(); } bindEvents() { this.banner?.addEventListener('click', (e) => { const target = e.target; if (target.dataset.action === 'accept-all') { this.acceptAll(); } else if (target.dataset.action === 'reject-all') { this.rejectAll(); } else if (target.dataset.action === 'customize') { this.showPreferences(); } }); } checkExistingConsent() { const stored = localStorage.getItem('ethical_consent'); if (stored) { const consent = JSON.parse(stored); const isExpired = Date.now() > consent.expiresAt; if (!isExpired) { this.hideBanner(); this.applyConsent(consent.choices); return; } } this.showBanner(); } acceptAll() { const choices = {}; this.purposes.forEach(p => { choices[p.id] = true; }); this.saveAndApply(choices); } rejectAll() { const choices = {}; this.purposes.forEach(p => { // Only necessary purposes are enabled choices[p.id] = p.necessary; }); this.saveAndApply(choices); } saveAndApply(choices) { const consent = { choices, timestamp: Date.now(), expiresAt: Date.now() + (this.behavior.rejectionDurationDays * 24 * 60 * 60 * 1000) }; localStorage.setItem('ethical_consent', JSON.stringify(consent)); this.applyConsent(choices); this.hideBanner(); // Dispatch event for other scripts window.dispatchEvent(new CustomEvent('consent:updated', { detail: { choices } })); } applyConsent(choices) { // Apply to Google Consent Mode if (typeof gtag !== 'undefined') { gtag('consent', 'update', { 'analytics_storage': choices.analytics ? 'granted' : 'denied', 'ad_storage': choices.advertising ? 'granted' : 'denied', 'ad_user_data': choices.advertising ? 'granted' : 'denied', 'ad_personalization': choices.advertising ? 'granted' : 'denied' }); } } showBanner() { if (this.banner) { this.banner.style.display = 'block'; // Focus first button for accessibility this.banner.querySelector('button')?.focus(); } } hideBanner() { if (this.banner) { this.banner.style.display = 'none'; } } showPreferences() { window.dispatchEvent(new CustomEvent('consent:showPreferences')); } } // Initialize when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => new EthicalConsentManager()); } else { new EthicalConsentManager(); } `; } private escapeHtml(text: string): string { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } } export { EthicalConsentBuilder, EthicalConsentConfig }; ``` ## Common Dark Patterns Reference Here's a comprehensive reference of dark patterns and their remediation: | Pattern | Description | Legal Status | Remediation | |---------|-------------|--------------|-------------| | **Confirm-shaming** | Guilt-inducing rejection text | Illegal (DSA, FTC, CPRA) | Use neutral "Decline" or "No thanks" | | **Hidden reject** | Reject option buried or invisible | Illegal (DSA, CPRA) | Equal prominence with Accept | | **Pre-checked boxes** | Non-essential options pre-selected | Illegal (GDPR, DSA) | All non-essential unchecked by default | | **Roach motel** | Easy to accept, hard to reject | Illegal (FTC, DSA) | Symmetric effort for both choices | | **Visual asymmetry** | Accept button larger/brighter | Illegal (DSA, CPRA) | Equal visual weight for all options | | **Nagging** | Repeated prompts after rejection | Illegal (DSA) | Respect rejection for session/period | | **Fake urgency** | "Offer expires in 5 minutes" | Illegal (FTC) | Remove artificial time pressure | | **Misdirection** | Ambiguous labels like "OK" | Risky (FTC deception) | Clear "Accept"/"Decline" labels | ## Frequently Asked Questions ### What penalties can businesses face for using dark patterns? EU DSA violations can result in fines up to 6% of global annual turnover. FTC penalties under Section 5 can reach $50,000+ per violation per affected consumer. California AG has enforced penalties of $2,500-$7,500 per violation under CPRA. ### How do I know if my consent banner has dark patterns? Run the automated detection tools provided in this guide. Key indicators: Accept button more prominent than Reject, shame language in rejection text, pre-checked non-essential boxes, or reject option requiring more clicks than accept. ### Is it legal to show a cookie wall that blocks content? CNIL (France) and Austrian DPA have ruled cookie walls illegal as they don't represent freely-given consent. However, "accept or pay" models are still being debated. The safest approach is to allow content access regardless of consent choice. ### Can I A/B test different consent designs? Yes, but only within compliant parameters. You cannot A/B test dark patterns vs. ethical designs. You can test different compliant button colors, copy variations, or positioning while maintaining equal prominence. ### How often should I audit my consent interface? Quarterly audits are recommended, plus immediate review after any design changes. Regulations evolve—what was acceptable in 2024 may not be in 2025. Automated monitoring catches issues between audits. ## Building Trust Through Ethical Design The dark pattern crackdown represents a fundamental shift in how regulators view user interface design. What was once a gray area of "aggressive optimization" is now clearly illegal manipulation. For forward-thinking organizations, this is an opportunity. Ethical consent interfaces build user trust, reduce complaint rates, and create sustainable marketing relationships. The short-term boost from manipulative designs isn't worth the legal, reputational, and relationship costs. Use the detection tools, compliance checkers, and building blocks provided in this guide to create consent experiences that respect users while meeting business objectives. The era of dark patterns is ending—organizations that lead the transition to ethical design will be best positioned for the privacy-first future.
L

Lisa Andersson, Regulatory Affairs

Kirjoittaja GetCookiesissa, erikoistunut tietosuojavaatimustenmukaisuuteen, hyväksyntähallintaan ja digitaalisen markkinoinnin optimointiin.

Valmis yksinkertaistamaan evästehyväksyntää?

GetCookies tekee GDPR:n, CCPA:n ja maailmanlaajuisen tietosuojavaatimustenmukaisuuden vaivattomaksi. Aloita tänään.