ObjectUIObjectUI

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
  1. You define a Theme object (colors, fonts, radii, spacing).
  2. ThemeProvider from @object-ui/react resolves inheritance and converts the theme to CSS custom properties injected on document.documentElement.
  3. Tailwind classes reference those properties (e.g. bg-primary maps to hsl(var(--primary))).
  4. Components use cva() for variant logic and cn() 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:

  1. Set the dir attribute on your root element:
<html dir="rtl" lang="ar">
  1. Use Tailwind's RTL modifier for directional overrides:
<div className="ps-4 pe-2 rtl:ps-2 rtl:pe-4">
  Directional padding
</div>
  1. Avoid left / right utilities — prefer start / end equivalents (text-start, ms-4, rounded-s-lg).

Note: RTL support requires Tailwind CSS v3.3+ or v4 with logical property utilities enabled.

On this page