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 /unstyled entry and skip CSS variables altogether.