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 fromThemableVariants); arbitrary string strings still typecheckbase?: 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.