Configuration
Configure and customize Nim UI for your project
Tailwind Configuration
Nim UI is built on Tailwind CSS v4, which is configured in CSS. There is no tailwind.config.js — v4 does not read one unless you point at it explicitly with @config, and a config file left lying around is simply ignored. Everything below goes in your entry stylesheet.
Basic Setup
@import 'tailwindcss';
@import '@nim-ui/components/styles';That is the whole setup. Tailwind scans your source for class names on its own; add @source only for a directory it would not find, such as a sibling package:
@source '../../shared-ui/src';Theme Customization
Every customization below is a @theme block. The variable name is the utility: define --color-brand-500 and bg-brand-500 exists.
Colors
Customize the color palette to match your brand:
@theme {
--color-primary-500: oklch(0.62 0.19 250);
--color-primary-600: oklch(0.54 0.19 250);
/* Or add a scale of your own */
--color-brand-50: oklch(0.97 0.02 300);
--color-brand-500: oklch(0.58 0.21 300);
--color-brand-900: oklch(0.28 0.11 300);
}Redefining --color-primary-* re-tints the kit itself, since its components reference those variables rather than fixed colours.
Typography
Customize fonts and type scale:
@theme {
--font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
--font-mono: 'Fira Code', ui-monospace, monospace;
--text-xs: 0.75rem;
--text-xs--line-height: 1rem;
--text-base: 1rem;
--text-base--line-height: 1.5rem;
}Line height rides along on the size as a --line-height suffix, rather than sitting in a second argument the way it did in v3.
Spacing
Tailwind derives its whole spacing scale from one base, so p-18 already works without being declared. Define a step only when you want a value off that rhythm:
@theme {
--spacing-18: 4.5rem;
--spacing-128: 32rem;
}Border Radius
@theme {
--radius-4xl: 2rem;
}Dark Mode Configuration
Nothing to configure. The kit's stylesheet already binds the dark: variant to a .dark class — and to [data-theme="dark"], so a docs framework's own toggle works too. Importing @nim-ui/components/styles is all it takes; a darkMode setting in a config file does nothing here.
The binding is not overridable from your own stylesheet: redefining @custom-variant dark has no effect whether you write it before or after the import — measured, the kit's definition wins either way and no prefers-color-scheme rule is emitted. To follow the operating system, keep the class and set it from JavaScript with matchMedia, as the toggle below does.
Dark Mode Toggle
Implement a dark mode toggle:
import { useEffect, useState } from 'react';
import { Button } from '@nim-ui/components';
export default function DarkModeToggle() {
const [darkMode, setDarkMode] = useState(() => {
// Check local storage or system preference
if (typeof window !== 'undefined') {
const stored = localStorage.getItem('darkMode');
if (stored !== null) return stored === 'true';
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
return false;
});
useEffect(() => {
// Apply dark mode class to html element
if (darkMode) {
document.documentElement.classList.add('dark');
localStorage.setItem('darkMode', 'true');
} else {
document.documentElement.classList.remove('dark');
localStorage.setItem('darkMode', 'false');
}
}, [darkMode]);
return (
<Button
variant="ghost"
onClick={() => setDarkMode(!darkMode)}
aria-label="Toggle dark mode"
>
{darkMode ? '🌙' : '☀️'}
</Button>
);
}CSS Customization
Custom Component Styles
Nim UI components render no stable class hook to @apply onto — there is no .nim-button (or equivalent) selector on any component, so a rule targeting one matches nothing.
Override a component's output through the className prop instead; every component merges it with its own classes through the kit's cn(), which resolves conflicting Tailwind classes correctly:
<Button className="rounded-full">Rounded button</Button>For a style you want to reuse across many instances, extend the component's CVA variants rather than reaching for a custom selector and @apply. See Customization for the full pattern — className overrides, extending CVA variants, and composing wrapper components.
CSS Variables
Nim UI's palette is not a set of hex custom properties redefined per scheme under :root and .dark. It is a single @theme block of OKLCH values, one per shade, declared in packages/ui/src/tokens.css and used identically in both themes — see Theme Customization above for how to override a shade in your own stylesheet, and Theming for the full mechanism. Dark-mode contrast comes from a component pairing a different shade number under the dark: variant (text-primary-600 dark:text-primary-400), not from a shade's own value changing under a class selector; the dark: binding itself is the @custom-variant covered in Dark Mode Configuration above.
For your own values that need to change at runtime rather than by theme — a user-configurable accent, say — plain CSS custom properties still apply; see Customization → CSS Custom Properties for that pattern.
Build Configuration
Vite
Optimize build for production:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
// Optimize chunk splitting
rollupOptions: {
output: {
manualChunks: {
'nim-ui': ['@nim-ui/components'],
},
},
},
},
optimizeDeps: {
include: ['@nim-ui/components'],
},
});Next.js
Configure Next.js for optimal performance:
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ['@nim-ui/components'],
compiler: {
removeConsole: process.env.NODE_ENV === 'production',
},
experimental: {
optimizePackageImports: ['@nim-ui/components'],
},
};
export default nextConfig;Webpack
For custom Webpack setups:
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader'],
},
],
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx'],
},
};Environment Variables
Configure environment-specific settings:
# API endpoints
VITE_API_URL=https://api.example.com
# Feature flags
VITE_ENABLE_DARK_MODE=true
VITE_ENABLE_ANALYTICS=falseAccess in your code:
const apiUrl = import.meta.env.VITE_API_URL;
const darkModeEnabled = import.meta.env.VITE_ENABLE_DARK_MODE === 'true';TypeScript Configuration
Strict Type Checking
Enable strict mode for better type safety:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true
}
}Path Aliases
Set up path aliases for cleaner imports:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@components/*": ["./src/components/*"],
"@ui/*": ["./node_modules/@nim-ui/components/*"]
}
}
}Then use:
import { Button } from '@ui';
import Header from '@components/Header';Performance Optimization
Tree Shaking
Ensure tree shaking works correctly:
// ✅ Good - imports only Button
import { Button } from '@nim-ui/components';
// ❌ Bad - imports everything
import * as NimUI from '@nim-ui/components';Code Splitting
Split components into separate chunks:
import { lazy, Suspense } from 'react';
// Lazy load heavy components
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
}Bundle Analysis
Analyze your bundle size:
pnpm add -D rollup-plugin-visualizerimport { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
react(),
visualizer({ open: true }),
],
});pnpm add -D @next/bundle-analyzerconst withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({
// Your Next.js config
});Run with: ANALYZE=true pnpm build
What's Next?
- Quick Start - Build your first component
- Customization Guide - Advanced customization techniques
- Theming Guide - Create custom themes
- Best Practices - Performance and accessibility tips