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-toolkit

Components

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

PropTypeDefaultDescription
optionsConsentOption[]RequiredArray of consent options to display
onSave(settings: ConsentSettings) => voidRequiredCallback 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.
titlestring'We Value Your Privacy'Title displayed on the banner
descriptionstring'We use cookies...'Description text explaining the purpose of cookies
acceptAllButtonTextstring'Accept All'Text for the accept all button
rejectAllButtonTextstring'Reject All'Text for the reject all button
customizeButtonTextstring'Customize'Text for the customize button
storageKeystring'ndpr_consent'localStorage key used to persist consent
showbooleanundefinedExplicitly control banner visibility. When omitted, the banner auto-shows if no saved consent exists.
versionstring'1.0'Consent form version. Banner re-shows when stored version differs.
classNamestringundefinedAdditional CSS class on the banner root element
classNamesConsentBannerClassNamesundefinedPer-slot class overrides for granular styling
unstyledbooleanfalseRemove all default Tailwind classes to style from scratch
onAnalytics(event: ConsentAnalyticsEvent) => voidundefinedAnalytics callback fired on each user interaction (shown, accepted_all, rejected_all, customized, dismissed)

ConsentManager Props

PropTypeDefaultDescription
optionsConsentOption[]RequiredArray of consent options to display
onSave(settings: ConsentSettings) => voidRequiredCallback called when the user saves their preferences
settingsConsentSettingsundefinedCurrent consent settings to pre-populate the form
titlestring'Manage Your Privacy Settings'Title displayed in the manager
descriptionstring'Update your consent preferences...'Description text displayed in the manager
saveButtonTextstring'Save Preferences'Text for the save button
resetButtonTextstring'Reset to Defaults'Text for the reset button
versionstring'1.0'Version stamped on saved ConsentSettings
classNamestringundefinedAdditional CSS class on the root element
classNamesConsentManagerClassNamesundefinedPer-slot class overrides for granular styling
unstyledbooleanfalseRemove 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-labelledby pointing at the title and aria-describedby pointing at the description. When variant="modal" or position="center", aria-modal="true" is set so screen readers treat it as a true modal.
  • Focus trap and restore: the shared useFocusTrap hook (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 dismissed event), closes the banner, and triggers focus restoration.
  • Reduced motion: the bundled styles.css includes 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 require aria-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:

GitHub Issues

Report bugs or request features on our GitHub repository.

View Issues

NDPA Resources

Learn more about NDPA 2023 compliance requirements.

NDPA Framework