npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-class-variants

v2.0.0-alpha.9

Published

Type-safe React variants API for dynamic CSS class composition

Readme

React Class Variants

npm version npm bundle size TypeScript License: MIT

react-class-variants is a recipe-first, type-safe API for composing CSS classes in React components.

This documentation describes the current react-class-variants v2.x surface. If you are looking for the legacy react-tailwind-variants v1.x docs, start with the legacy v1 docs entrypoint.

The current v2 alpha surface is built around:

  • one adaptive recipe() primitive
  • one React builder, styled(), exposed through defineConfig()
  • explicit view-consumed component props through defineViewProps()
  • explicit render polymorphism
  • explicit merge configuration through defineConfig()
  • a dedicated core subpath for recipe-only modules

Status

  • Package name: react-class-variants
  • Current line: 2.0.0-alpha.x
  • Recommended install: react-class-variants@alpha
  • Runtime requirements: Node.js 20.19+ and React 19
  • Module format: ESM-only

Installation

pnpm add react-class-variants@alpha

Optional Tailwind conflict resolution:

pnpm add tailwind-merge

Mental Model

| Need | Use | | ------------------------------------------ | ----------------------------------- | | Compute one root class string | recipe(input) | | Compute slot class strings | recipe(input).slotName() | | Split variant props from a full prop bag | recipe.resolve(input, options) | | Build a React component from a recipe | const { styled } = defineConfig() | | Declare component props consumed by view | defineViewProps<T>(...keys) | | Share merge or validate behavior | defineConfig(options) | | Keep typed config objects around | defineRecipeConfig(config) | | Avoid React runtime imports | react-class-variants/core |

Four rules explain most of the package:

  1. recipe() becomes a root recipe when you use base, and a slotted recipe when you use slots.
  2. resolve() is the full-prop-bag API. Direct recipe calls stay variant-oriented.
  3. Get styled() from defineConfig(); it accepts intrinsic bases and custom React component bases.
  4. Slotted recipes require view, and render is opt-in through withRender: true for intrinsic bases only.

Quick Start

import { defineConfig, recipe } from 'react-class-variants';

const { styled } = defineConfig();

const buttonRecipe = recipe({
  base: 'inline-flex items-center justify-center rounded-md font-medium transition',
  variants: {
    tone: {
      primary: 'bg-blue-600 text-white hover:bg-blue-700',
      ghost: 'bg-transparent text-slate-900 hover:bg-slate-100',
    },
    size: {
      sm: 'h-8 px-3 text-sm',
      md: 'h-10 px-4 text-base',
    },
  },
  defaultVariants: {
    tone: 'primary',
    size: 'md',
  },
});

export const Button = styled('button', buttonRecipe);

Usage:

<Button tone="ghost" size="sm" type="button">
  Cancel
</Button>

Direct recipe calls stay available when you only need a class string:

buttonRecipe({ tone: 'primary', className: 'w-full' });

Keeping Config Objects Explicit

Use defineRecipeConfig() when you want the config itself to stay available as a typed source of truth, for example to power Storybook controls, docs, or test fixtures from the same declared variants:

import {
  defineRecipeConfig,
  recipe,
  variantNames,
  variantOptions,
} from 'react-class-variants';

export const badgeConfig = defineRecipeConfig({
  base: 'inline-flex items-center rounded-full font-medium',
  variants: {
    tone: {
      info: 'bg-sky-100 text-sky-800',
      success: 'bg-emerald-100 text-emerald-800',
      danger: 'bg-red-100 text-red-800',
    },
    size: {
      sm: 'px-2 py-0.5 text-xs',
      md: 'px-2.5 py-1 text-sm',
    },
  },
  defaultVariants: {
    tone: 'info',
    size: 'md',
  },
});

export const badgeRecipe = recipe(badgeConfig);

export const badgeToneOptions = variantOptions(badgeConfig, 'tone');
export const badgeVariantNames = variantNames(badgeRecipe);

defineRecipeConfig() is a zero-cost typed helper. Reach for it when you keep a config object in a variable and want defaultVariants completions plus exact key/value checks. In v2, recipe instances do not expose a runtime .config property, so this is the intended way to keep config data around.

Tailwind CSS IntelliSense in VS Code

If you use the Tailwind CSS IntelliSense extension, register the v2 recipe helpers in tailwindCSS.classFunctions so completions, hovers, and linting work inside recipe(...) and defineRecipeConfig(...) calls:

{
  "editor.quickSuggestions": {
    "strings": "on"
  },
  "tailwindCSS.classFunctions": ["recipe", "defineRecipeConfig"]
}

This is the setting that covers class strings in base, slots, variant branches, and compoundVariants[].className inside JavaScript and TypeScript config objects.

If you rename a configured factory, add that alias too:

const { recipe: uiRecipe } = defineConfig();
{
  "tailwindCSS.classFunctions": ["recipe", "defineRecipeConfig", "uiRecipe"]
}

Tailwind CSS IntelliSense matches function names, so wrapper or alias names must be listed explicitly. This still depends on the extension detecting your Tailwind project normally.

Slotted Components

Use slots when different parts of the component need different classes, then render them through a view component:

import { defineConfig, recipe } from 'react-class-variants';

const { styled } = defineConfig();

const buttonRecipe = recipe({
  slots: {
    root: 'inline-flex items-center gap-2 rounded-md font-medium',
    icon: 'size-4',
    label: 'truncate',
  },
  variants: {
    tone: {
      primary: {
        root: 'bg-blue-600 text-white',
        icon: 'text-blue-100',
      },
      ghost: {
        root: 'bg-transparent text-slate-900',
        icon: 'text-slate-500',
      },
    },
  },
  defaultVariants: {
    tone: 'primary',
  },
});

function ButtonView({ host, classes }) {
  return host.render({
    children: (
      <>
        <span aria-hidden="true" className={classes.icon()} />
        <span className={classes.label()}>{host.children}</span>
      </>
    ),
  });
}

const Button = styled('button', buttonRecipe, {
  withRender: true,
  view: ButtonView,
});

Usage:

<Button tone="ghost" className="w-full">
  Cancel
</Button>

<Button
  tone="primary"
  slotClassNames={{
    icon: 'text-red-500',
    label: 'uppercase',
  }}
>
  Save
</Button>

<Button tone="primary" render={<a href="/docs" />}>
  Docs
</Button>

External non-host slot overrides use slotClassNames:

const slots = buttonRecipe({
  tone: 'primary',
  slotClassNames: {
    icon: 'text-red-500',
  },
});

const resolved = buttonRecipe.resolve({
  tone: 'primary',
  className: 'w-full',
  slotClassNames: {
    label: 'uppercase',
  },
  id: 'save',
});

Key points:

  • view is required for slotted recipes
  • view is a normal React component, so hooks and context work inside it
  • prefer a named component such as ButtonView when you use hooks
  • use defineViewProps() when a view needs component-level props such as icon, startIcon, endIcon, or shortcut; those props stay on host.props and are stripped before the rendered host receives its props
  • use classes.slotName() for the most direct slot lookup in view
  • classes is still an enumerable slot render map and may be safely destructured when that reads better
  • top-level slotClassNames applies to matching slots, while top-level className still routes only to the host slot
  • slotClassNames keys should match declared slot names
  • slotClassNames is consumed before props are forwarded, while local slot-function className still wins for that one slot call
  • external component className is routed automatically to the host slot
  • if the recipe has no root slot, provide hostSlot with one of the declared slot names
  • call host.render(...) directly as a method
  • do not destructure render from host
  • this is intentional: keeping host.render method-shaped avoids allocating one extra function per view render

Inline view functions are still fine for trivial cases. A named component is easier for hook linting, React DevTools, and stack traces once the view starts using hooks.

resolve() and Wrappers

Use recipe.resolve() when you need class resolution plus a full prop bag:

  • wrapper components
  • headless abstractions
  • host prop aliasing such as propAliases: { size: 'htmlSize' }
  • explicit forwarding of resolved variant values with forwardProps

Example:

import type { ComponentPropsWithoutRef } from 'react';
import { recipe, type VariantProps } from 'react-class-variants';

const inputRecipe = recipe({
  base: 'block w-full rounded-md border',
  variants: {
    size: {
      sm: 'h-9 px-3 text-sm',
      md: 'h-10 px-4 text-base',
    },
    invalid: {
      true: 'border-red-500 ring-1 ring-red-500',
    },
  },
  defaultVariants: {
    size: 'md',
    invalid: false,
  },
});

type InputProps = Omit<ComponentPropsWithoutRef<'input'>, 'size'> &
  VariantProps<typeof inputRecipe> & {
    htmlSize?: number;
  };

export function Input(props: InputProps) {
  const resolved = inputRecipe.resolve(props, {
    propAliases: {
      size: 'htmlSize',
    },
  });

  return (
    <input
      {...resolved.resolvedProps}
      aria-invalid={resolved.variants.invalid || undefined}
    />
  );
}

Usage:

<Input size="sm" htmlSize={20} invalid type="email" />

What resolve() gives you here:

  • resolved.resolvedProps is the host-ready prop bag, including the merged className
  • propAliases lets the public API accept htmlSize while resolvedProps receives the native size prop
  • resolved.variants gives you the effective variant selection after defaults and boolean fallbacks
  • for slot recipes, resolve() returns slots plus resolvedProps instead of one root className
  • for slot recipes, top-level slotClassNames applies to matching slots and is consumed before props are forwarded

Alias names must not collide with existing host props, reserved React public props, or declared variant keys.

Shared Configuration

Use defineConfig() when you want one configured factory for many recipes:

import { defineConfig } from 'react-class-variants';
import { twMerge } from 'tailwind-merge';

export const { recipe, styled } = defineConfig({
  merge: twMerge,
});

Supported options:

type SystemOptions = {
  merge?: (className: string) => string;
  validate?: 'never' | 'always';
};

Default root and core imports are lean and process-less safe. Use defineConfig({ validate: 'always' }) when you want checked runtime behavior for a shared factory.

Utilities

Package root also exports a few low-level helpers for wrappers and polymorphic components:

import {
  hasOwnProperty,
  mergeProps,
  mergeRefs,
  useMergeRefs,
} from 'react-class-variants';
  • hasOwnProperty(object, key) is a typed own-property guard and is also available from react-class-variants/core
  • mergeProps(base, overrides) concatenates className, shallow-merges style, composes React event handlers with override-first ordering, and replaces other props with the override value
  • mergeRefs(...refs) creates a merged ref callback for non-hook contexts such as cloneElement() or conditional branches
  • useMergeRefs(...refs) is the memoized hook form for React components

Primary APIs

Most users will work with these package-root APIs from react-class-variants:

  • recipe()
  • defineConfig() for configured recipe() and styled()
  • defineRecipeConfig()
  • variantNames() and variantOptions()
  • defineViewProps()
  • public core and React types

Package root also exports mergeProps, mergeRefs, useMergeRefs, and hasOwnProperty for lower-level integration work.

For recipe-only modules, the primary react-class-variants/core APIs are:

  • recipe()
  • core defineConfig()
  • defineRecipeConfig()
  • variantNames() and variantOptions()
  • core recipe types

Use the core subpath when you want recipe modules without React runtime helpers.

Where to Read Next

If you are new to the package, use this order:

  1. Recipes and components guide for the main usage patterns
  2. API reference for exact shapes, options, and runtime rules
  3. Migration guide if you are moving from react-tailwind-variants

Other docs: