Files
skills/docs/ui/react/react-ui/src/theme/use-themed-variants/use-themed-variants.md
2026-05-29 08:20:56 +01:00

3.3 KiB

useThemedVariants() — resolve theme-aware cva variants

(no submodules)

Purpose

Resolves the active theme's variant overrides for a named component and returns a memoised cva resolver. Components that compose with the chassis theme system call this hook rather than importing a component's buttonVariants directly so a downstream theme can swap or extend the cva config without forking the source. The hook implements the type-safety trade-off documented in the cross- cutting design §4.2 — default-typed calls return base variants only; narrowed calls allow brand-new variants with a documented runtime fall-through.

Features

  • Reads the active <ThemeProvider> context for the live theme
  • Merges theme.variantOverrides[componentName] on top of the base cva resolver supplied as the second argument
  • Memoised per (themeName, componentName) so repeated renders share one resolver reference
  • OTel histogram react_ui.themed_variants.resolve_duration_ms{component} on cache miss
  • Default-typed call returns the base cva surface
  • Narrowed call (useThemedVariants<'dracula', 'Button'>('Button')) exposes brand-new theme-specific variants at the type level

Usage: Basic example

import { useThemedVariants } from "@suaveplan/react-ui/theme/use-themed-variants";
import { cva } from "class-variance-authority";

const buttonVariants = cva("inline-flex h-9 items-center", {
    variants: { size: { sm: "px-3", md: "px-4" } },
    defaultVariants: { size: "md" },
});

function MyButton() {
    const variants = useThemedVariants("Button", buttonVariants);
    return <button className={variants({ size: "md" })} type="button" />;
}

Usage: Advanced example

import { useThemedVariants } from "@suaveplan/react-ui/theme/use-themed-variants";

declare module "@suaveplan/react-ui/theme" {
    interface ThemableVariants {
        readonly Button: (props?: { size?: "sm" | "md" | "lg" }) => string;
    }
}

// Default-typed: only base sm/md/lg are reachable
const v = useThemedVariants("Button");
v({ size: "lg" });

// Narrowed: brand-new dracula-only `xl` size compiles, but
// silently falls through to undefined when the active theme is
// not 'dracula' (documented runtime behaviour).
const dv = useThemedVariants<"dracula", "Button">("Button");
dv({ size: "xl" } as { size: "xl" });

API Reference

  • useThemedVariants<TThemeName, TComponentName>(componentName, base?)
    • componentName: registered component key (autocompleted from ThemableVariants); arbitrary string strings still typecheck
    • base?: CvaResolver — base resolver used when no theme override exists; defaults to a no-op resolver that returns ''
    • Returns the typed resolver per the type-safety trade-off

Implementation notes

The cache lives in a useRef<CachedEntry | null> so the resolver reference is stable across renders for the same (theme, component) tuple. The merge strategy is intentionally simple — when the theme declares an override for the named component, the override resolver WINS outright. Consumers needing layered semantics (override augments base rather than replacing) can use cva's own compose() primitive inside their override declaration. The histogram only fires on a cache MISS so the steady-state read is zero-cost.