// styles/global.css
@import "tailwindcss";
@import "kovax-react/tailwind";
/*
* The preset generates utilities like:
* bg-kx-primary-500 -> background-color: var(--kx-color-primary-500)
* p-kx-md -> padding: var(--kx-spacing-md)
* rounded-kx-lg -> border-radius: var(--kx-radius-lg)
*/
Architecture Rationale: Using @theme inline instead of a static configuration file prevents token duplication. When ThemeProvider updates --kx-color-primary-500 for dark mode, Tailwind utilities automatically reflect the change without requiring a CSS rebuild or runtime class swapping.
Form libraries manage state differently, but UI components expect a consistent context shape. Kovax provides thin adapter packages that translate library-specific state into the FormControl context (isInvalid, isRequired, isDisabled).
React Hook Form Integration:
import { useForm } from "react-hook-form";
import { FormField, FormFieldError } from "kovax-react/react-hook-form";
import { FormControl, FormLabel, TextInput, Button, Stack } from "kovax-react";
interface CheckoutData {
cardNumber: string;
expiryDate: string;
}
export function PaymentForm() {
const { control, handleSubmit } = useForm<CheckoutData>({
mode: "onTouched",
defaultValues: { cardNumber: "", expiryDate: "" }
});
const onSubmit = handleSubmit((data) => console.log("Processing:", data));
return (
<form onSubmit={onSubmit} className="flex flex-col gap-kx-lg">
<FormField control={control} name="cardNumber" rules={{ required: "Card number is required" }}>
<FormControl>
<FormLabel>Card Number</FormLabel>
<TextInput placeholder="0000 0000 0000 0000" />
<FormFieldError />
</FormControl>
</FormField>
<FormField control={control} name="expiryDate" rules={{ required: true, pattern: /^\d{2}\/\d{2}$/ }}>
<FormControl>
<FormLabel>Expiry (MM/YY)</FormLabel>
<TextInput placeholder="MM/YY" />
<FormFieldError />
</FormControl>
</FormField>
<Button type="submit" variant="solid" color="primary" fullWidth>
Complete Payment
</Button>
</form>
);
}
Architecture Rationale: The adapter package is optional and tree-shakeable. Developers only install the adapter matching their form library. The FormField component subscribes to the library's state manager and injects validation flags into FormControl, eliminating manual register/controller boilerplate while preserving full type safety.
Step 3: Server-Side Theme Persistence
Theme flash occurs when JavaScript initializes the palette after the browser paints the default light/dark scheme. The solution injects a blocking script in the <head> that reads localStorage and prefers-color-scheme before the first render.
// app/layout.tsx
import { ColorModeScript } from "kovax-react/server";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<ColorModeScript storageKey="app-theme-pref" />
</head>
<body>
{children}
</body>
</html>
);
}
// app/providers.tsx
"use client";
import { ThemeProvider } from "kovax-react";
export function AppProviders({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider storageKey="app-theme-pref" defaultColorMode="system">
{children}
</ThemeProvider>
);
}
Architecture Rationale: The script and provider must share the exact storageKey. The server-side script sets data-kovax-theme on the <html> element synchronously. The client-side ThemeProvider reads the same key to maintain state continuity. This two-phase approach guarantees zero visual discrepancy during hydration.
Step 4: Component Catalog & Testing
A unified architecture requires rigorous visual regression testing. Kovax ships with a Storybook configuration that resolves source files via Vite aliases, enabling live development without intermediate build steps. The setup includes autoDocs for TypeScript prop extraction, @storybook/addon-a11y for automated accessibility auditing, and Chromatic integration for pixel-perfect baseline comparisons.
Pitfall Guide
1. Mismatched Storage Keys Between Script and Provider
Explanation: The ColorModeScript and ThemeProvider use different storageKey values, causing the server to set one theme while the client initializes another. This results in a hydration mismatch or delayed theme application.
Fix: Extract the key into a shared constant file and import it in both the server layout and client provider. Validate that both components reference the exact same string.
2. Overriding @theme inline with Static Tailwind Configuration
Explanation: Adding duplicate token definitions in tailwind.config.js creates conflicting CSS variable resolutions. Tailwind v4 prioritizes inline theme definitions, but static config overrides can cause utilities to reference undefined variables.
Fix: Remove all token-related extensions from tailwind.config.js. Rely exclusively on the kovax-react/tailwind preset for variable mapping. Use @theme only for framework-specific overrides that don't conflict with design tokens.
3. Neglecting Peer Dependency Installation
Explanation: Form adapters and Tailwind integration are peer dependencies. Omitting them causes runtime module resolution failures or silent context breakdowns.
Fix: Explicitly install react-hook-form or @tanstack/react-form alongside the corresponding kovax-react adapter package. Verify peer requirements in package.json before deployment.
4. Client-Side Theme Initialization
Explanation: Initializing theme state inside a useEffect or component body delays palette application until after the first paint, causing FOUC.
Fix: Always use ColorModeScript or buildColorModeScriptTag() in the document <head>. Never rely on client-side hooks for initial theme resolution.
5. Ignoring Form Context Propagation in Nested Components
Explanation: Placing FormField inside custom wrappers without forwarding context breaks the isInvalid/isRequired chain. Validation errors appear visually but don't trigger accessibility attributes.
Fix: Ensure custom form wrappers use React.forwardRef and spread ...rest props. If building composite inputs, explicitly pass context values from FormField down to child components.
6. Storybook Vite Alias Misconfiguration
Explanation: Storybook fails to resolve component source files, falling back to compiled distribution builds. This breaks hot module replacement and prop documentation generation.
Fix: Configure viteFinal in .storybook/main.ts to alias kovax-react to the src/ directory. Verify that TypeScript paths align with the alias configuration.
7. Mixing Utility Classes with Inline Styles for Tokens
Explanation: Applying style={{ backgroundColor: 'var(--kx-color-primary-500)' }} alongside Tailwind utilities creates specificity conflicts and bypasses the reactive theme system.
Fix: Commit fully to the utility class system (bg-kx-primary-500). Reserve inline styles only for dynamic values that cannot be expressed as tokens.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| New project with React Hook Form | kovax-react/react-hook-form adapter | Eliminates 60% of form boilerplate, maintains type safety | Low (single package install) |
| Legacy app migrating to Tailwind v4 | @theme inline preset + CSS variable mapping | Prevents token duplication, ensures reactive palette swaps | Medium (requires config cleanup) |
| High-traffic e-commerce checkout | Server-side ColorModeScript + ThemeProvider | Guarantees zero FOUC, improves LCP and CLS metrics | Low (inline script overhead < 2KB) |
| Multi-framework design system | Shared --kx-* tokens + framework-specific adapters | Decouples design tokens from implementation details | High (initial architecture setup) |
| Internal dashboard with minimal forms | Skip form adapters, use native FormControl | Reduces bundle size when validation complexity is low | Low (no adapter dependency) |
Configuration Template
// shared/theme-config.ts
export const THEME_STORAGE_KEY = "app-color-mode";
export const DEFAULT_COLOR_MODE = "system";
// app/layout.tsx
import { ColorModeScript } from "kovax-react/server";
import { THEME_STORAGE_KEY } from "@/shared/theme-config";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<ColorModeScript storageKey={THEME_STORAGE_KEY} />
</head>
<body>
{children}
</body>
</html>
);
}
// app/providers.tsx
"use client";
import { ThemeProvider } from "kovax-react";
import { THEME_STORAGE_KEY, DEFAULT_COLOR_MODE } from "@/shared/theme-config";
export function AppProviders({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider
storageKey={THEME_STORAGE_KEY}
defaultColorMode={DEFAULT_COLOR_MODE}
>
{children}
</ThemeProvider>
);
}
// styles/global.css
@import "tailwindcss";
@import "kovax-react/tailwind";
Quick Start Guide
- Install core dependencies: Run
npm install kovax-react@0.8.0 tailwindcss and optionally npm install react-hook-form or @tanstack/react-form based on your stack.
- Configure CSS: Add
@import "kovax-react/tailwind"; to your root stylesheet after the Tailwind import. Remove any conflicting token definitions from your Tailwind config.
- Initialize theme persistence: Place
<ColorModeScript storageKey="your-key" /> in your server layout's <head>. Wrap your application with <ThemeProvider storageKey="your-key" defaultColorMode="system"> in the client entry point.
- Wire forms: Replace manual validation context with
<FormField control={control} name="field"> wrappers. Import FormFieldError to display validation messages automatically.
- Verify integration: Run
npm run dev and inspect the DOM. Confirm that data-kovax-theme is present before hydration, Tailwind utilities resolve var(--kx-*) correctly, and form validation flags propagate to UI components without manual prop drilling.