Compound Components

Compose full UI flows from small, focused sub-components using the v3 compound component pattern

What Are Compound Components?

The v3 toolkit exposes every module as a compound component — a namespace object whose properties are individual, purpose-built sub-components that share state through React context. You assemble them like building blocks rather than configuring a single monolithic component via props.

This pattern gives you:

  • Full control over layout — sub-components render wherever you place them in JSX
  • Selective rendering — only mount the pieces you actually need
  • Easy custom wrappers — wrap any sub-component with your own UI without forking library code
  • Predictable state — all sub-components within a Provider share a single source of truth

The Provider + Sub-components Pattern

Every module follows the same structure. The Provider establishes context and accepts configuration. All other sub-components must be rendered inside a Provider.

// Pattern:
<Module.Provider config={...}>
  <Module.SubComponentA />
  <Module.SubComponentB />
  <Module.SubComponentC />
</Module.Provider>

Sub-components do not need to be direct children — they can be deeply nested inside your own layout components as long as the Provider is an ancestor in the React tree.

Building Custom Layouts

Because sub-components are independent, you can build arbitrarily complex layouts. The following example puts consent preferences in a modal dialog rather than a bottom banner:

import { Consent } from '@tantainnovative/ndpr-toolkit';
import { Dialog } from '@/components/ui/Dialog';
import { useState } from 'react';

export function ConsentModal() {
  const [open, setOpen] = useState(false);

  return (
    <Consent.Provider categories={categories}>
      {/* A plain button that opens the modal */}
      <button onClick={() => setOpen(true)}>Privacy Settings</button>

      <Dialog open={open} onOpenChange={setOpen}>
        <Dialog.Content>
          <Consent.Title>Privacy Preferences</Consent.Title>
          <Consent.Description>
            Manage how we use your data.
          </Consent.Description>
          <Consent.CategoryList className="my-6" />
          <div className="flex justify-end gap-2">
            <Consent.RejectAllButton />
            <Consent.SaveButton onSave={() => setOpen(false)} />
          </div>
        </Dialog.Content>
      </Dialog>
    </Consent.Provider>
  );
}

You can also access the shared context directly with module-specific hooks to build fully bespoke UI:

import { useConsentContext } from '@tantainnovative/ndpr-toolkit';

// Must be rendered inside <Consent.Provider>
export function CustomConsentToggle({ categoryId }: { categoryId: string }) {
  const { consents, updateConsent } = useConsentContext();
  const enabled = consents[categoryId] ?? false;

  return (
    <label>
      <input
        type="checkbox"
        checked={enabled}
        onChange={(e) => updateConsent(categoryId, e.target.checked)}
      />
      Custom {categoryId} toggle
    </label>
  );
}

Example: DSR Module

The DSR compound component handles Data Subject Request submissions and status tracking. The DSR.Form sub-component orchestrates the multi-step request form whileDSR.Tracker lets users follow up on submitted requests.

import { DSR } from '@tantainnovative/ndpr-toolkit';

const dsrConfig = {
  organizationName: 'Acme Ltd',
  contactEmail: 'privacy@acme.com',
  requestTypes: ['access', 'deletion', 'rectification', 'portability'],
  onSubmit: async (request) => {
    const res = await fetch('/api/dsr', {
      method: 'POST',
      body: JSON.stringify(request),
    });
    return res.json(); // { requestId: 'DSR-001' }
  },
};

export function DSRPage() {
  return (
    <DSR.Provider config={dsrConfig}>
      {/* Submission form — renders request type selector, identity
          fields, and details textarea automatically */}
      <DSR.Form
        onSuccess={(requestId) =>
          console.log('Request submitted:', requestId)
        }
      />

      {/* Optional: let returning users check request status */}
      <DSR.Dashboard className="mt-8" />
    </DSR.Provider>
  );
}

// --- Or track a single known request ---
export function DSRStatusPage({ requestId }: { requestId: string }) {
  return (
    <DSR.Provider config={dsrConfig}>
      <DSR.Tracker requestId={requestId} />
    </DSR.Provider>
  );
}

Example: DPIA Module

The DPIA compound component guides privacy teams through a Data Protection Impact Assessment. The DPIA.StepIndicator shows progress, while DPIA.Report renders the generated assessment summary.

import { DPIA } from '@tantainnovative/ndpr-toolkit';

const dpiaConfig = {
  projectName: 'Customer Analytics Pipeline',
  assessor: 'Data Protection Officer',
  onComplete: (assessment) => {
    console.log('DPIA complete, risk level:', assessment.riskLevel);
  },
};

export function DPIAWorkflow() {
  return (
    <DPIA.Provider config={dpiaConfig}>
      {/* Step progress bar: "Describe Processing → Assess Necessity →
          Identify Risks → Mitigations → Sign-off" */}
      <DPIA.StepIndicator className="mb-6" />

      {/* Renders the active step's questions */}
      <DPIA.Questionnaire />

      {/* Once all steps complete, show the final risk report */}
      <DPIA.Report className="mt-8" />
    </DPIA.Provider>
  );
}

Example: Breach Module

The Breach compound component covers the full data-breach response workflow — initial report, risk assessment, and regulatory notification drafting — all wired together through a shared Provider context.

import { Breach } from '@tantainnovative/ndpr-toolkit';

const breachConfig = {
  organizationName: 'Acme Ltd',
  dpoEmail: 'dpo@acme.com',
  regulatoryBody: 'NDPC',           // Nigeria Data Protection Commission
  notificationDeadlineHours: 72,    // NDPA 2023 requirement
  onSubmit: async (report) => {
    await fetch('/api/breach-reports', {
      method: 'POST',
      body: JSON.stringify(report),
    });
  },
};

export function BreachResponsePage() {
  return (
    <Breach.Provider config={breachConfig}>
      {/* Step 1: Capture initial breach details */}
      <Breach.ReportForm />

      {/* Step 2: Guided risk assessment (severity, affected subjects) */}
      <Breach.RiskAssessment className="mt-6" />

      {/* Step 3: Manage notifications to affected individuals */}
      <Breach.NotificationManager className="mt-6" />

      {/* Step 4: Generate the formatted regulatory report */}
      <Breach.ReportGenerator className="mt-6" />
    </Breach.Provider>
  );
}

NDPA 72-hour Requirement

The NDPA 2023 requires controllers to notify the Nigeria Data Protection Commission within 72 hours of becoming aware of a qualifying breach. The notificationDeadlineHours config value drives the countdown timer surfaced by Breach.NotificationManager.

All 8 Modules and Their Sub-components

Every module follows the same compound component pattern. The table below lists the available sub-components for each module. All are accessible from @tantainnovative/ndpr-toolkit.

ModuleImportSub-components
ConsentConsentProvider, Banner, Settings, Storage, OptionList, AcceptButton, RejectButton, SaveButton
DSRDSRProvider, Form, Dashboard, Tracker
DPIADPIAProvider, Questionnaire, Report, StepIndicator
BreachBreachProvider, ReportForm, RiskAssessment, NotificationManager, ReportGenerator
PolicyPolicyProvider, Generator, Preview, Exporter
LawfulBasisLawfulBasisProvider, Tracker
CrossBorderCrossBorderProvider, Manager
ROPAROPAProvider, Manager

Unstyled Mode

Every sub-component accepts an unstyled prop that strips all default Tailwind classes. Pass your own via className to integrate with any design system:

<Consent.Provider categories={categories} unstyled>
  <Consent.Banner className="my-custom-banner">
    <Consent.CategoryList className="my-category-list" />
    <Consent.AcceptAllButton className="my-btn my-btn--primary" />
  </Consent.Banner>
</Consent.Provider>