Theming with NDPRThemeProvider
Two ways to theme: a typed React Context provider that injects --ndpr-* CSS variables from a JavaScript object, or direct CSS-variable overrides. Same end result; pick what fits your codebase.
Overview
Every visual aspect of the toolkit is driven by --ndpr-* CSS custom properties defined in @tantainnovative/ndpr-toolkit/styles. New in 3.10.0, NDPRThemeProvider is a thin React Context wrapper that sets those variables from a typed JavaScript theme object — useful when your design tokens already live in JS / TS.
The provider is syntactic sugar. Underneath, components still consume the same CSS variables — so you can mix-and-match: use the provider for the bulk of the theme, then layer per-section overrides via plain CSS.
Quickstart
import { NDPRThemeProvider, type NDPRTheme } from '@tantainnovative/ndpr-toolkit';
import '@tantainnovative/ndpr-toolkit/styles';
const theme: NDPRTheme = {
colors: {
primary: '22 163 74', // RGB triplet — green-600
primaryHover: '21 128 61', // green-700
},
radius: { base: '0.75rem' },
font: { sans: '"Inter", system-ui, sans-serif' },
};
export default function App() {
return (
<NDPRThemeProvider theme={theme}>
<YourApp />
</NDPRThemeProvider>
);
}The provider renders a single <div> wrapper with the variables set inline. Unset fields fall through to the stylesheet defaults.
Colour values are RGB triplets
All colour tokens take the format 'R G B' (space-separated channels, no rgb() wrapper). The stylesheet wraps them asrgb(var(--ndpr-primary)), which lets the same token drive solid backgrounds and transparent tints like rgb(var(--ndpr-primary) / 0.12).
If your design system stores hex, add a one-liner converter:
const hexToRgbTriplet = (hex: string) => {
const n = parseInt(hex.replace('#', ''), 16);
return `${(n >> 16) & 255} ${(n >> 8) & 255} ${n & 255}`;
};
const theme: NDPRTheme = {
colors: { primary: hexToRgbTriplet('#16a34a') },
};Full NDPRTheme reference
Every field is optional and maps to one --ndpr-* CSS variable.
interface NDPRTheme {
mode?: 'light' | 'dark';
colors?: {
primary?: string; // --ndpr-primary
primaryHover?: string; // --ndpr-primary-hover
primaryForeground?: string; // --ndpr-primary-foreground
background?: string; // --ndpr-background
surface?: string; // --ndpr-surface
foreground?: string; // --ndpr-foreground
muted?: string; // --ndpr-muted
mutedForeground?: string; // --ndpr-muted-foreground
border?: string; // --ndpr-border
input?: string; // --ndpr-input
ring?: string; // --ndpr-ring
success?: string; // --ndpr-success
destructive?: string; // --ndpr-destructive
warning?: string; // --ndpr-warning
};
radius?: { sm?: string; base?: string; lg?: string; full?: string };
spacing?: { 1?: string; 2?: string; 3?: string; 4?: string; 5?: string; 6?: string; 8?: string };
shadow?: { sm?: string; base?: string; lg?: string };
font?: {
sans?: string;
sizeXs?: string; sizeSm?: string; sizeBase?: string;
sizeLg?: string; sizeXl?: string;
lineHeight?: string; lineHeightTight?: string;
};
transition?: { base?: string; slow?: string };
z?: { banner?: number | string; modal?: number | string };
}Dark mode
Setting mode: 'dark' stamps data-theme="dark" on the wrapper, which activates the dark-mode block in the stylesheet. The toolkit also auto-switches via prefers-color-scheme, so most apps don't need to set this explicitly.
const [mode, setMode] = useState<'light' | 'dark'>('light');
<NDPRThemeProvider theme={{ mode, ...rest }}>
<button onClick={() => setMode(m => m === 'dark' ? 'light' : 'dark')}>
Toggle theme
</button>
<App />
</NDPRThemeProvider>When NOT to use the provider
- You already manage design tokens in CSS — write
:root { --ndpr-primary: 22 163 74; }directly. - You need to scope theme to a subtree without a wrapping element — apply
--ndpr-*overrides to your existing ancestor. - You're bringing your own design system entirely — use the
/unstyledentry and skip CSS variables altogether.