Dark Pattern Legislation: Global Crackdown in 2025
Lisa Andersson, Regulatory AffairsOctober 16, 202516 min de leitura
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 `
${this.escapeHtml(copy.title)}
${this.escapeHtml(copy.description)}
`;
}
// 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
Escritor no GetCookies, especializado em conformidade de privacidade, gestão de consentimento e otimização de marketing digital.