Theming & Customization
Customize the look-and-feel of ObjectUI components with CSS custom properties, Tailwind CSS, cva variants, and runtime theme switching
Theming & Customization
ObjectUI's theming system is built on three layers: CSS custom properties (design tokens), Tailwind CSS (utility classes), and class-variance-authority (component variants). Together they give you full control over appearance — from global palettes down to individual component states.
How Theming Works
Theme JSON → ThemeProvider → CSS Custom Properties → Tailwind utilities → Components- You define a Theme object (colors, fonts, radii, spacing).
ThemeProviderfrom@object-ui/reactresolves inheritance and converts the theme to CSS custom properties injected ondocument.documentElement.- Tailwind classes reference those properties (e.g.
bg-primarymaps tohsl(var(--primary))). - Components use
cva()for variant logic andcn()for class merging.
CSS Custom Properties
ObjectUI follows the Shadcn convention. Design tokens are defined as HSL channel values on :root and .dark:
/* globals.css */
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--border: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
/* ... remaining dark tokens */
}
}Components reference these tokens through Tailwind:
<div className="bg-background text-foreground border-border" />
<button className="bg-primary text-primary-foreground" />Tailwind Configuration
Extend your tailwind.config.js to map tokens to Tailwind utilities:
// tailwind.config.js
export default {
darkMode: "class",
content: [
"./src/**/*.{ts,tsx}",
"./node_modules/@object-ui/components/dist/**/*.js",
],
theme: {
extend: {
colors: {
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
border: "hsl(var(--border))",
ring: "hsl(var(--ring))",
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
fontFamily: {
sans: ["Inter", "ui-sans-serif", "system-ui"],
},
},
},
};Component Variants with cva
ObjectUI uses class-variance-authority to define type-safe component variants. Each component exports a *Variants function alongside the component itself:
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@object-ui/components";
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors",
{
variants: {
variant: {
default: "border-transparent bg-primary text-primary-foreground",
secondary: "border-transparent bg-secondary text-secondary-foreground",
destructive: "border-transparent bg-destructive text-destructive-foreground",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
);
interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
}To add a custom variant to an existing component, wrap it:
const statusVariants = cva("", {
variants: {
status: {
active: "bg-green-500/15 text-green-700 border-green-500/25",
pending: "bg-yellow-500/15 text-yellow-700 border-yellow-500/25",
inactive: "bg-gray-500/15 text-gray-500 border-gray-500/25",
},
},
});
function StatusBadge({ status, className, ...props }: { status: "active" | "pending" | "inactive" } & React.HTMLAttributes<HTMLDivElement>) {
return <Badge className={cn(statusVariants({ status }), className)} variant="outline" {...props} />;
}Class Overrides with cn()
The cn() utility from @object-ui/components combines clsx and tailwind-merge so that later classes win over earlier ones without producing duplicate utilities:
import { cn } from "@object-ui/components";
// tailwind-merge resolves conflicts: "p-4" wins over "p-2"
cn("p-2 bg-red-500", "p-4");
// → "bg-red-500 p-4"
// Conditional classes with clsx syntax
cn("base-class", isActive && "bg-primary", className);Every ObjectUI component accepts a className prop that is merged via cn(), so consumers can always override styles:
<Button className="rounded-full px-8" variant="outline">
Custom Shape
</Button>Dark Mode
ObjectUI supports three modes: light, dark, and auto (follows system preference). The ThemeProvider applies a light or dark class to the root element and listens for prefers-color-scheme changes.
import { ThemeProvider } from "@object-ui/react";
function App() {
return (
<ThemeProvider defaultMode="auto">
<MyApp />
</ThemeProvider>
);
}Toggle mode at runtime with the useTheme hook:
import { useTheme } from "@object-ui/react";
function ModeToggle() {
const { resolvedMode, setMode } = useTheme();
return (
<button onClick={() => setMode(resolvedMode === "dark" ? "light" : "dark")}>
{resolvedMode === "dark" ? "☀️ Light" : "🌙 Dark"}
</button>
);
}Ensure your Tailwind config uses darkMode: "class" so that .dark scoped utilities work correctly.
Custom Component Themes
Register one or more theme definitions with ThemeProvider. Each theme is a plain JSON object:
import type { Theme } from "@object-ui/types";
const corporateTheme: Theme = {
name: "corporate",
colors: {
primary: "220 70% 50%",
"primary-foreground": "0 0% 100%",
background: "0 0% 100%",
foreground: "220 20% 10%",
accent: "200 80% 55%",
"accent-foreground": "0 0% 100%",
},
fonts: {
sans: "IBM Plex Sans, system-ui",
},
radius: "0.375rem",
};
<ThemeProvider themes={[corporateTheme]} defaultTheme="corporate">
<App />
</ThemeProvider>Themes support inheritance via the extends field. A child theme only needs to declare its overrides:
const darkCorporate: Theme = {
name: "corporate-dark",
extends: "corporate",
colors: {
background: "220 20% 8%",
foreground: "0 0% 95%",
},
};
<ThemeProvider themes={[corporateTheme, darkCorporate]} defaultTheme="corporate-dark">
<App />
</ThemeProvider>Creating a Custom Color Palette
Start from HSL values and define both light and dark variants:
const brand: Theme = {
name: "brand",
colors: {
// Light palette
primary: "262 83% 58%", // Purple
"primary-foreground": "0 0% 100%",
secondary: "262 30% 94%",
"secondary-foreground": "262 83% 30%",
accent: "160 84% 39%", // Teal accent
"accent-foreground": "0 0% 100%",
background: "0 0% 100%",
foreground: "262 20% 10%",
muted: "262 20% 95%",
"muted-foreground": "262 10% 45%",
border: "262 20% 90%",
ring: "262 83% 58%",
destructive: "0 84% 60%",
"destructive-foreground": "0 0% 100%",
},
radius: "0.5rem",
};Tip: Use the Shadcn Themes tool to generate a full palette, then paste the HSL values into your Theme object.
Runtime Theme Switching
ThemeProvider accepts multiple themes. Switch between them at runtime with setTheme:
import { useTheme } from "@object-ui/react";
function ThemeSwitcher() {
const { activeTheme, themes, setTheme } = useTheme();
return (
<select value={activeTheme ?? ""} onChange={(e) => setTheme(e.target.value)}>
{themes.map((t) => (
<option key={t.name} value={t.name}>{t.name}</option>
))}
</select>
);
}Enable persistence so the user's choice survives page reloads:
<ThemeProvider
themes={[lightTheme, darkTheme, brandTheme]}
defaultTheme="light"
persist
storageKey="my-app-theme"
>
<App />
</ThemeProvider>The provider stores the active theme name and mode in localStorage under the keys <storageKey>-name and <storageKey>-mode.
RTL Support
ObjectUI components use logical CSS properties (ms-, me-, ps-, pe-) where available via Tailwind's logical utilities. For full RTL support:
- Set the
dirattribute on your root element:
<html dir="rtl" lang="ar">- Use Tailwind's RTL modifier for directional overrides:
<div className="ps-4 pe-2 rtl:ps-2 rtl:pe-4">
Directional padding
</div>- Avoid
left/rightutilities — preferstart/endequivalents (text-start,ms-4,rounded-s-lg).
Note: RTL support requires Tailwind CSS v3.3+ or v4 with logical property utilities enabled.