Consent Management
NDPA 2023-compliant consent management system for handling user consent preferences
Overview
The Consent Management component provides a complete solution for collecting, storing, and managing user consent in compliance with the Nigeria Data Protection Act 2023 (NDPA). It includes a customizable consent banner, preference management interface, and consent storage system.
NDPA Consent Requirements
Under the NDPA 2023, consent must be freely given, specific, informed, and unambiguous. The data subject must clearly indicate acceptance through a statement or clear affirmative action. Pre-ticked boxes or silence do not constitute valid consent.
Quick Start
The toolkit ships zero-config presets, compound components for custom layouts, and a StorageAdapter pattern so you can plug in any persistence backend without touching component internals.
Zero-config preset
// Drop in a fully-working consent banner with NDPA defaults
import { NDPRConsent } from '@tantainnovative/ndpr-toolkit/presets';
<NDPRConsent />Compound components for custom layouts
import { Consent } from '@tantainnovative/ndpr-toolkit/consent';
import { apiAdapter } from '@tantainnovative/ndpr-toolkit/adapters';
<Consent.Provider options={options} adapter={apiAdapter('/api/consent')}>
<Consent.Banner />
</Consent.Provider>Hook with adapter
import { useConsent } from '@tantainnovative/ndpr-toolkit/hooks';
import { memoryAdapter } from '@tantainnovative/ndpr-toolkit/adapters';
// The adapter prop accepts any StorageAdapter for custom persistence
// (localStorage, sessionStorage, API, IndexedDB, etc.)
const consent = useConsent({ options, adapter: memoryAdapter() });Installation
Install the NDPR Toolkit package which includes the Consent Management components:
pnpm add @tantainnovative/ndpr-toolkitComponents
The Consent Management system includes several components that work together:
ConsentBanner
A cookie consent banner that appears at the bottom of the page when a user first visits your site. Fully customizable with support for multiple consent options.
import { ConsentBanner } from '@tantainnovative/ndpr-toolkit';
<ConsentBanner
options={[
{
id: 'necessary',
label: 'Necessary Cookies',
description: 'Essential cookies for the website to function.',
required: true,
purpose: 'Core website functionality'
},
{
id: 'analytics',
label: 'Analytics Cookies',
description: 'Cookies that help us understand how you use our website.',
required: false,
purpose: 'Usage analytics'
},
{
id: 'marketing',
label: 'Marketing Cookies',
description: 'Cookies used for marketing purposes.',
required: false,
purpose: 'Marketing and advertising'
}
]}
onSave={(settings) => console.log(settings)}
position="bottom"
/>ConsentManager
A higher-order component that manages the consent state and provides methods for checking and updating consent. Works with the useConsent hook to provide a complete consent management solution.
import { ConsentManager, useConsent } from '@tantainnovative/ndpr-toolkit';
const options = [
{
id: 'necessary',
label: 'Necessary Cookies',
description: 'Essential cookies for the website to function.',
required: true,
purpose: 'Core website functionality'
},
{
id: 'analytics',
label: 'Analytics Cookies',
description: 'Cookies that help us understand how you use our website.',
required: false,
purpose: 'Usage analytics'
}
];
function PreferencesPage() {
const { settings, updateConsent, resetConsent } = useConsent({ options });
return (
<ConsentManager
options={options}
settings={settings ?? undefined}
onSave={(newSettings) => {
// Persist or forward the updated ConsentSettings
updateConsent(newSettings.consents);
}}
/>
);
}
function AnalyticsLoader() {
const { hasConsent } = useConsent({ options });
if (hasConsent('analytics')) {
// Initialize analytics
}
return null;
}ConsentStorage
A component for handling the storage and retrieval of consent settings. Supports both local storage and custom storage mechanisms.
import { ConsentStorage } from '@tantainnovative/ndpr-toolkit';
import type { ConsentSettings } from '@tantainnovative/ndpr-toolkit';
import { useState } from 'react';
function ConsentStorageExample() {
const [settings, setSettings] = useState<ConsentSettings>({
consents: { necessary: true, analytics: false, marketing: false },
timestamp: Date.now(),
version: '1.0',
method: 'explicit',
hasInteracted: true,
});
const handleLoad = (loadedSettings: ConsentSettings | null) => {
if (loadedSettings) {
setSettings(loadedSettings);
}
};
return (
<ConsentStorage
settings={settings}
storageOptions={{
storageKey: 'my-app-consent',
storageType: 'localStorage'
}}
onLoad={handleLoad}
onSave={(savedSettings) => console.log('Saved:', savedSettings)}
autoLoad={true}
autoSave={true}
>
{({ saveSettings, clearSettings, loaded }) => (
<div>
<p>Consent settings loaded: {loaded ? 'Yes' : 'No'}</p>
<button onClick={() => saveSettings(settings)}>Save Settings</button>
<button onClick={() => clearSettings()}>Clear Settings</button>
</div>
)}
</ConsentStorage>
);
}Usage
Here's a complete example of how to implement the consent management system in your application:
import { useState, useEffect } from 'react';
import {
ConsentBanner,
ConsentManager,
useConsent
} from '@tantainnovative/ndpr-toolkit';
const consentOptions = [
{
id: 'necessary',
label: 'Necessary Cookies',
description: 'Essential cookies for the website to function.',
required: true,
purpose: 'Core website functionality'
},
{
id: 'analytics',
label: 'Analytics Cookies',
description: 'Cookies that help us understand how you use our website.',
required: false,
purpose: 'Usage analytics'
},
{
id: 'marketing',
label: 'Marketing Cookies',
description: 'Cookies used for marketing purposes.',
required: false,
purpose: 'Marketing and advertising'
}
];
function App() {
const [showPreferences, setShowPreferences] = useState(false);
const consent = useConsent({ options: consentOptions });
return (
<div>
<header>
<nav>
<button onClick={() => setShowPreferences(true)}>Cookie Preferences</button>
</nav>
</header>
<main>{/* Your main content */}</main>
{showPreferences && (
<PreferencesModal
consent={consent}
onClose={() => setShowPreferences(false)}
/>
)}
<ConsentBanner
options={consentOptions}
onSave={(settings) => consent.updateConsent(settings.consents)}
position="bottom"
/>
</div>
);
}
function PreferencesModal({ consent, onClose }) {
const { settings, updateConsent, resetConsent } = consent;
const handleSave = () => {
// settings are already saved via updateConsent
onClose();
};
return (
<div className="modal">
<div className="modal-content">
<h2>Cookie Preferences</h2>
{consentOptions.map(option => (
<label key={option.id}>
<input
type="checkbox"
checked={settings?.consents[option.id] || false}
disabled={option.required}
onChange={(e) =>
updateConsent({
...settings?.consents,
[option.id]: e.target.checked
})
}
/>
{option.label}
</label>
))}
<button onClick={handleSave}>Save</button>
<button onClick={resetConsent}>Reset</button>
<button onClick={onClose}>Cancel</button>
</div>
</div>
);
}
function AnalyticsComponent() {
const { hasConsent } = useConsent({ options: consentOptions });
useEffect(() => {
if (hasConsent('analytics')) {
console.log('Analytics initialized');
}
}, [hasConsent]);
return null;
}Banner variants
The variant prop (added in v3.4.0) controls visual treatment. Pair it with position for placement.
variant="bar" (default)
Full-width strip pinned to the top or bottom edge.
<ConsentBanner
options={options}
onSave={handleSave}
position="bottom"
/>variant="card"
Bounded floating card with rounded corners and margin from the screen edges. Polished default for marketing and product sites.
<ConsentBanner
options={options}
onSave={handleSave}
variant="card"
position="bottom"
/>variant="modal"
Centered card with a backdrop overlay. Forces center placement regardless of position. Use sparingly — modals interrupt the page and are rarely the right choice for compliance widgets.
<ConsentBanner
options={options}
onSave={handleSave}
variant="modal"
/>Targeting variants from your CSS
Each rendered banner exposes data-ndpr-component, data-ndpr-variant, and data-ndpr-position attributes you can target without depending on internal class names.
/* Customise the card variant only */
[data-ndpr-component="consent-banner"][data-ndpr-variant="card"] {
border-radius: 1rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
}
/* Special-case the modal overlay */
[data-ndpr-component="consent-banner"][data-ndpr-variant="modal"] {
background: rgba(0, 0, 0, 0.7);
}API Reference
ConsentBanner Props
| Prop | Type | Default | Description |
|---|---|---|---|
| options | ConsentOption[] | Required | Array of consent options to display |
| onSave | (settings: ConsentSettings) => void | Required | Callback function called when consent is saved |
| position | 'top' | 'bottom' | 'center' | 'inline' | 'bottom' | Position of the banner on the page. top/bottom/center render via a portal; inline renders in normal document flow. |
| variant | 'bar' | 'card' | 'modal' | 'bar' | Visual treatment. bar is the default full-width strip. card renders a bounded floating card with margin from the screen edges (pairs well with position="bottom"). modal forces center placement with a backdrop overlay regardless of position. Added in v3.4.0. |
| title | string | 'We Value Your Privacy' | Title displayed on the banner |
| description | string | 'We use cookies...' | Description text explaining the purpose of cookies |
| acceptAllButtonText | string | 'Accept All' | Text for the accept all button |
| rejectAllButtonText | string | 'Reject All' | Text for the reject all button |
| customizeButtonText | string | 'Customize' | Text for the customize button |
| storageKey | string | 'ndpr_consent' | localStorage key used to persist consent |
| show | boolean | undefined | Explicitly control banner visibility. When omitted, the banner auto-shows if no saved consent exists. |
| version | string | '1.0' | Consent form version. Banner re-shows when stored version differs. |
| className | string | undefined | Additional CSS class on the banner root element |
| classNames | ConsentBannerClassNames | undefined | Per-slot class overrides for granular styling |
| unstyled | boolean | false | Remove all default Tailwind classes to style from scratch |
| onAnalytics | (event: ConsentAnalyticsEvent) => void | undefined | Analytics callback fired on each user interaction (shown, accepted_all, rejected_all, customized, dismissed) |
ConsentManager Props
| Prop | Type | Default | Description |
|---|---|---|---|
| options | ConsentOption[] | Required | Array of consent options to display |
| onSave | (settings: ConsentSettings) => void | Required | Callback called when the user saves their preferences |
| settings | ConsentSettings | undefined | Current consent settings to pre-populate the form |
| title | string | 'Manage Your Privacy Settings' | Title displayed in the manager |
| description | string | 'Update your consent preferences...' | Description text displayed in the manager |
| saveButtonText | string | 'Save Preferences' | Text for the save button |
| resetButtonText | string | 'Reset to Defaults' | Text for the reset button |
| version | string | '1.0' | Version stamped on saved ConsentSettings |
| className | string | undefined | Additional CSS class on the root element |
| classNames | ConsentManagerClassNames | undefined | Per-slot class overrides for granular styling |
| unstyled | boolean | false | Remove all default Tailwind classes to style from scratch |
ConsentOption Type
interface ConsentOption {
id: string;
label: string;
description: string;
required: boolean;
/** NDPA Section 25(2): specific purpose for which data will be processed */
purpose: string;
defaultValue?: boolean;
dataCategories?: string[];
}ConsentSettings Type
interface ConsentSettings {
/** Map of consent option IDs to boolean consent values */
consents: Record<string, boolean>;
/** Unix timestamp of when consent was last updated */
timestamp: number;
/** Version of the consent form that was accepted */
version: string;
/** Method used to collect consent: 'banner' | 'manager' | 'explicit' | 'customize' */
method: string;
/** Whether the user actively made a choice */
hasInteracted: boolean;
}ConsentStorageOptions Type
interface ConsentStorageOptions {
/** Storage key — defaults to 'ndpr_consent' */
storageKey?: string;
/** Storage backend — defaults to 'localStorage' */
storageType?: 'localStorage' | 'sessionStorage' | 'cookie';
cookieOptions?: {
domain?: string;
path?: string;
/** Expiration in days — defaults to 365 */
expires?: number;
secure?: boolean;
sameSite?: 'Strict' | 'Lax' | 'None';
};
}useConsent Hook
import { useConsent } from '@tantainnovative/ndpr-toolkit';
import { localStorageAdapter } from '@tantainnovative/ndpr-toolkit/adapters';
const {
settings, // ConsentSettings | null — current saved settings
hasConsent, // (optionId: string) => boolean — check per-option consent
updateConsent, // (consents: Record<string, boolean>) => void
acceptAll, // () => void — mark every option as true
rejectAll, // () => void — mark non-required options as false
shouldShowBanner, // boolean — true when no valid consent is stored
isValid, // boolean — current settings pass validation
validationErrors, // string[] — validation error messages
resetConsent, // () => void — clear stored consent
isLoading, // boolean — true while an async adapter is loading
} = useConsent({
options,
// v3 approach (recommended): plug in any StorageAdapter
adapter: localStorageAdapter('my-consent-key'),
// v2 approach (deprecated, still works):
// storageOptions: { storageKey: 'my-consent-key' },
version: '1.0',
onChange: (settings) => console.log('Consent changed:', settings),
});Best Practices
- Clear Language:Use clear, plain language to explain what each type of cookie does and why you're collecting the data.
- No Pre-selected Options:Don't pre-select non-essential cookies. The NDPA requires that consent is freely given and pre-selected checkboxes don't constitute valid consent.
- Easy Access to Preferences: Make it easy for users to access and update their consent preferences at any time.
- Consent Records: Keep records of when and how consent was obtained. The ConsentManager component automatically tracks consent history.
- Respect User Choices: Only activate cookies and tracking technologies when the user has given explicit consent for them.
Accessibility
The ConsentBanner is keyboard-accessible, screen-reader friendly, and respects user motion preferences. The following guarantees are baked into the component itself — you do not need to add ARIA attributes or wire up focus handling.
- Dialog semantics: the banner renders with
role="dialog",aria-labelledbypointing at the title andaria-describedbypointing at the description. Whenvariant="modal"orposition="center",aria-modal="true"is set so screen readers treat it as a true modal. - Focus trap and restore: the shared
useFocusTraphook (3.5.4+) captures the element that had focus before the banner opened, moves focus into the banner, cycles Tab and Shift+Tab within it, and restores focus to the original element on close (WCAG 2.4.3). - Escape dismisses: pressing Escape while the banner is open rejects all non-required consents (firing an analytics
dismissedevent), closes the banner, and triggers focus restoration. - Reduced motion: the bundled
styles.cssincludes a@media (prefers-reduced-motion: reduce)block that neutralises animations and transitions on every.ndpr-*element (WCAG 2.3.3). Users with vestibular-motion sensitivity see the banner appear without movement. - Labelled controls: every checkbox in the customise panel has an associated
<label htmlFor>, and required options are marked with a visible asterisk alongside the disabled checkbox. - Accessible button names: Accept All, Reject All, Customize, Save Preferences and Back are real
<button>elements with text content — no icon-only buttons that requirearia-label.
Keyboard interaction
Tab and Shift+Tab cycle through Accept All, Reject All, and Customize (and the checkboxes plus Save / Back when the panel is open) without escaping the banner. Enter activates the focused button. Escape closes the banner and is treated as "reject all non-required".
If you customise the styling
The toolkit ships the ARIA attributes, focus-trap logic, Escape handling and the reduced-motion CSS — these continue to work regardless of unstyled or classNames. However, when you opt into unstyled or override classNames, you become responsible for preserving visible focus states, sufficient color contrast (WCAG AA: 4.5:1 for text, 3:1 for UI components), spacing, and any motion-sensitive transitions you add on top.
Need Help?
If you have questions about implementing the Consent Management system or need assistance with NDPA compliance, check out these resources: