CM component library
npm install @vreaab/cm

A GDPR-compliant React consent management library with Google Consent Mode V2 integration.
- GDPR Compliance - Equal button prominence for Accept/Reject options, no dark patterns
- Google Analytics 4 Integration - Automatic Consent Mode V2 parameter updates
- Safari Private Mode Support - Fallback storage adapter using cookies when localStorage is unavailable
- Tri-State Consent Model - Distinguishes between granted, denied, and unset states
- Internationalization Ready - Full i18n support via react-intl
- Consent Banner for first-time visitors
- Preferences Modal for granular control
- Floating Cookie Trigger for returning users
- Google Consent Mode V2 integration
- Revocation detection with auto-reload countdown
- Storage adapter with localStorage + cookie fallback
- Full TypeScript support
- SSR/Next.js compatible with hydration safety
``bash`
npm install @vreaab/cm
- React 18 or 19
- React DOM 18 or 19
Wrap your application with ConsentProvider and add the consent components:
`tsx
import {
ConsentProvider,
ConsentBanner,
ConsentPreferencesModal,
ConsentTrigger,
} from '@vreaab/cm';
function App() {
return (
{/ Your app content /}
{/ Consent UI components /}
);
}
`
The root provider component that manages consent state.
`tsx`
messages={messages} // Optional: Custom translations
>
{children}
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| locale | string | 'en' | Locale code for internationalization |messages
| | Record | {} | Custom message translations keyed by message ID |children
| | ReactNode | Required | Application content |
Displays automatically for new visitors. Hides after user makes a choice.
`tsx`
No props required. Visibility is controlled by consent state.
Detailed preferences dialog for granular category control.
`tsx`
No props required. Open/close state is managed by the context.
Floating icon button for returning users to access preferences.
`tsx`
No props required. Appears only after user has made an initial choice.
Access consent state and actions from any component.
`tsx
const {
// State
consentState, // Current consent for all categories
hasInteracted, // Whether user has made a choice
isLoading, // True during initial load
// Actions
updateConsent, // Update a single category
acceptAll, // Accept all categories
rejectOptional, // Accept only necessary
openPreferences, // Show preferences modal
// Modal State
isPreferencesOpen,
setIsPreferencesOpen,
// Notifications
showToast, // Display a toast message
} = useConsent();
`
#### Example: Conditional Content
`tsx
function AnalyticsSection() {
const { consentState } = useConsent();
if (consentState.analytics !== true) {
return
Enable analytics cookies to see usage statistics.
; return
}
`
| Category | Required | Description | Google Consent Mode Parameters |
|----------|----------|-------------|-------------------------------|
| necessary | Yes | Essential cookies for site functionality | None (always allowed) |analytics
| | No | Usage tracking and statistics | analytics_storage |marketing
| | No | Advertising and remarketing | ad_storage, ad_user_data, ad_personalization |preferences
| | No | User settings and personalization | functionality_storage |
Each category uses a tri-state model:
| Value | Meaning |
|-------|---------|
| true | User explicitly granted consent |false
| | User explicitly denied consent |null
| | User has not made a choice yet |
Add the Google Analytics script to your HTML head. The library automatically manages consent parameters.
`html`
1. On page load, all consent parameters are set to 'denied' by defaultgtag('consent', 'update', {...})
2. When the user makes a choice, parameters are updated via
3. Google Analytics respects these parameters automatically
For stricter privacy, load analytics scripts only after consent:
`tsx
function AnalyticsScript() {
const { consentState } = useConsent();
useEffect(() => {
if (consentState.analytics === true) {
// Load GA script dynamically
const script = document.createElement('script');
script.src = 'https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX';
script.async = true;
document.head.appendChild(script);
}
}, [consentState.analytics]);
return null;
}
`
The library uses CSS custom properties for theming, scoped under the [data-cm] selector to avoid conflicts with your application's styles.
Override the default theme by targeting the [data-cm] selector:
`css
/ Light mode customization /
[data-cm] {
--cm-background: #ffffff;
--cm-foreground: #1a1a1a;
--cm-primary: #3b82f6;
--cm-primary-foreground: #ffffff;
--cm-muted: #f5f5f5;
--cm-muted-foreground: #737373;
--cm-border: #e5e5e5;
}
/ Dark mode customization /
.dark [data-cm],
[data-cm].dark {
--cm-background: #1a1a1a;
--cm-foreground: #fafafa;
--cm-primary: #60a5fa;
--cm-primary-foreground: #1a1a1a;
--cm-muted: #262626;
--cm-muted-foreground: #a3a3a3;
--cm-border: #404040;
}
`
| Variable | Description |
|----------|-------------|
| --cm-background | Main background color |--cm-foreground
| | Main text color |--cm-primary
| | Primary action color (buttons) |--cm-primary-foreground
| | Text on primary color |--cm-secondary
| | Secondary action color |--cm-secondary-foreground
| | Text on secondary color |--cm-muted
| | Muted/subtle background |--cm-muted-foreground
| | Muted text color |--cm-accent
| | Accent/hover color |--cm-accent-foreground
| | Text on accent color |--cm-destructive
| | Destructive action color |--cm-border
| | Border color |--cm-input
| | Input field border/background |--cm-ring
| | Focus ring color |--cm-radius
| | Border radius base value |
All library styles are:
- Prefixed with cm: to avoid class name conflicts[data-cm]
- Important to override conflicting host styles
- Scoped via CSS variables under
This ensures the consent UI renders correctly regardless of your application's CSS framework or global styles.
`tsx`
{/ Swedish locale /}
`tsx
const swedishMessages = {
'consent.banner.title': 'Cookiemedgivande',
'consent.banner.description': 'Vi använder cookies för att förbättra din upplevelse...',
'consent.actions.acceptAll': 'Acceptera alla',
'consent.actions.rejectOptional': 'Avvisa valfria',
'consent.actions.customize': 'Anpassa',
// ... more translations
};
{children}
`
| Message ID | Default Text |
|------------|--------------|
| consent.banner.title | Cookie Consent |consent.banner.description
| | We use cookies to enhance your experience... |consent.actions.acceptAll
| | Accept All |consent.actions.rejectOptional
| | Reject Optional |consent.actions.customize
| | Customize |consent.actions.learnMore
| | Learn more |consent.modal.title
| | Cookie Preferences |consent.modal.description
| | Manage your cookie preferences... |consent.actions.save
| | Save Preferences |consent.actions.cancel
| | Cancel |consent.category.required
| | Required |consent.revocation.title
| | Page Reload Required |consent.revocation.description
| | You have disabled some cookies... |consent.revocation.countdown
| | Reloading in {seconds}s... |consent.actions.reloadNow
| | Reload Now |consent.unsavedChanges.title
| | Unsaved Changes |consent.unsavedChanges.description
| | You have unsaved changes... |consent.actions.discardChanges
| | Discard Changes |consent.actions.keepEditing
| | Keep Editing |consent.feedback.saved
| | Your cookie preferences have been saved. |consent.trigger.label
| | Manage cookie preferences |consent.category.necessary.label
| | Necessary |consent.category.necessary.description
| | Essential for the website to function... |consent.category.analytics.label
| | Analytics |consent.category.analytics.description
| | Help us understand how visitors use... |consent.category.marketing.label
| | Marketing |consent.category.marketing.description
| | Used to track visitors across websites... |consent.category.preferences.label
| | Preferences |consent.category.preferences.description
| | Remember your settings and preferences... |
Full type definitions are included. Key exports:
`tsx`
import type {
ConsentCategory, // 'necessary' | 'analytics' | 'marketing' | 'preferences'
ConsentValue, // true | false | null
ConsentState, // Record of all category values
ConsentCategoryConfig // Category metadata with gtag mappings
} from '@vreaab/cm';
`tsx
// Consent value: tri-state model
type ConsentValue = true | false | null;
// Available categories
type ConsentCategory = 'necessary' | 'analytics' | 'marketing' | 'preferences';
// Full consent state
interface ConsentState {
necessary: ConsentValue;
analytics: ConsentValue;
marketing: ConsentValue;
preferences: ConsentValue;
}
// Category configuration
interface ConsentCategoryConfig {
id: ConsentCategory;
label: string;
description: string;
required: boolean;
gtagParams: string[];
}
`
| Browser | Version | Notes |
|---------|---------|-------|
| Chrome | 90+ | Full support |
| Firefox | 88+ | Full support |
| Safari | 14+ | localStorage fallback to cookies in private mode |
| Edge | 90+ | Full support |
Consent data is stored with versioned keys to support future migrations:
| Key | Description |
|-----|-------------|
| cm_v1_hasInteracted | Whether user has made any choice |cm_v1_consent_{category}
| | Consent value per category |cm_v1_timestamp
| | ISO timestamp of last consent update |
`bash`
git clone https://github.com/vrea/cm.git
cd cm
npm install
npm run dev
`bash`
npm run build
```
src/
├── components/consent/ # React UI components
│ ├── ConsentBanner.tsx
│ ├── ConsentPreferencesModal.tsx
│ ├── ConsentProvider.tsx
│ └── ConsentTrigger.tsx
├── lib/
│ ├── consent/ # Core logic
│ │ ├── consent-engine.ts
│ │ ├── gtag-integration.ts
│ │ ├── messages.ts
│ │ ├── storage-adapter.ts
│ │ └── types.ts
│ └── react/ # React exports
│ └── index.ts
└── globals.css # Tailwind styles
MIT License - see LICENSE for details.
Copyright (c) 2025 Jonathan Yngfors