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
Providershare 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.
Example: Consent Module
Here is a complete consent banner built from Consent.* sub-components. Notice how the layout is entirely controlled by your JSX — the library provides the logic and accessible markup, you provide the arrangement.
import { Consent } from '@tantainnovative/ndpr-toolkit';
import { localStorageAdapter } from '@tantainnovative/ndpr-toolkit';
const categories = [
{ id: 'necessary', label: 'Necessary', description: 'Core site functionality', required: true },
{ id: 'analytics', label: 'Analytics', description: 'Understand visitor behaviour' },
{ id: 'marketing', label: 'Marketing', description: 'Personalised advertising' },
];
export function MyConsentBanner() {
return (
<Consent.Provider
categories={categories}
adapter={localStorageAdapter}
onSave={(consents) => console.log('Saved:', consents)}
>
{/* The banner slides up from the bottom */}
<Consent.Banner position="bottom">
{/* Heading + description live here */}
<Consent.Title>We value your privacy</Consent.Title>
<Consent.Description>
We use cookies to improve your experience. You can customise your choices below.
</Consent.Description>
{/* Renders a toggle for each category */}
<Consent.CategoryList />
{/* Action buttons — layout determined by your CSS */}
<div className="flex gap-3 mt-4">
<Consent.AcceptAllButton className="btn btn-primary" />
<Consent.RejectAllButton className="btn btn-outline" />
<Consent.SaveButton className="btn btn-secondary" />
</div>
</Consent.Banner>
{/* Floating trigger to re-open preferences */}
<Consent.PreferencesButton />
</Consent.Provider>
);
}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.
| Module | Import | Sub-components |
|---|---|---|
| Consent | Consent | Provider, Banner, Settings, Storage, OptionList, AcceptButton, RejectButton, SaveButton |
| DSR | DSR | Provider, Form, Dashboard, Tracker |
| DPIA | DPIA | Provider, Questionnaire, Report, StepIndicator |
| Breach | Breach | Provider, ReportForm, RiskAssessment, NotificationManager, ReportGenerator |
| Policy | Policy | Provider, Generator, Preview, Exporter |
| LawfulBasis | LawfulBasis | Provider, Tracker |
| CrossBorder | CrossBorder | Provider, Manager |
| ROPA | ROPA | Provider, 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>