reclassify allows you to construct className strings directly in JSX without using libraries like clsx and classNames.
// Before:
<button className={
clsx(["btn", ["btn-primary", { "btn-disabled": false }], { "is-active": true }])
}>
Save
</button>
// After:
<button className={
// No need for clsx
["btn", ["btn-primary", { "btn-disabled": false }], { "is-active": true }]
}>
Save
</button>It constructs className strings for intrinsic elements only. Custom components keep their declared className prop types.
- No imports needed: You no longer have to import
clsxorclassnamesin every file, the JSX runtime handles classname construction automatically for all intrinsic elements. - Type-safe: TypeScript knows that
classNameon intrinsic elements accepts arrays, objects, and nested combinations. Noas stringcasts or loose typing. - Drop-in setup: One
tsconfig.jsonchange (jsxImportSource) and your entire app is covered. No Babel plugins, no wrappers, no HOCs. It's also backwards-compatible. - Familiar syntax: If you've used
clsx,classnames, Vue's:class, or Svelte'sclass:directive, the array/object pattern already feels natural.
npm install reclassify # Requires React >= 17 (automatic JSX runtime)<div className="plain-string" /> // Good ol' strings
<div className={["btn", "btn-primary"]} /> // Arrays
<div className={{ active: true, disabled: false }} /> // Objects
<div className={["btn", { active: isActive }, ["nested"]]} /> // Arrays containing objectsThere are two common ways to use reclassify, depending on whether you are using TypeScript or Babel to compile:
Set jsxImportSource when using the automatic JSX runtime:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "reclassify"
}
}You can also opt in per file:
/** @jsxImportSource reclassify */They will type-check cleanly.
Configure @babel/preset-react with the automatic runtime and importSource:
{
"presets": [
[
"@babel/preset-react",
{
"runtime": "automatic",
"importSource": "reclassify"
}
]
]
}Under the hood, reclassify is a custom JSX runtime for React that lets you pass arrays and objects as className on intrinsic elements.
When you set jsxImportSource: "reclassify" or Babel importSource: "reclassify", your JSX compiles against reclassify's runtime instead of React's default runtime. This is similar to how Preact works.
By default, reclassify uses clsx for className string construction.
At runtime, reclassify wraps React's jsx, jsxs, and jsxDEV functions and checks each element before it is created:
- If the element is an intrinsic element like
<div>or<button>,reclassifylooks at itsclassName.- If
classNameis already a string, the props are passed through unchanged. - If
classNameis an array, object, or other supported value,reclassifycallsclassify()to turn it into the final string React expects.
- If
- If you called
configure({ fn }),classifypoints to your provided function andreclassifyuses it instead of the defaultclsx-based implementation.
Custom components are not rewritten. They keep their existing className prop contract unless they call classify() themselves.
On the type side, reclassify also widens className for intrinsic JSX elements so TypeScript accepts the same array/object values that the runtime can classify.
Once configured, intrinsic elements accept arrays and objects for className with full TypeScript support — no type errors:
If you want to replace the built-in class construction function, before your app starts rendering JSX, call configure() once with your custom implementation. It returns a function that restores the previous construction function.
Here's an example using the cn util commonly-found in shadcn projects.
import { configure } from "reclassify";
import { cn } from "@/lib/utils";
const restore = configure({ fn: cn });
// Later, if needed (e.g. in tests):
restore();If you want to use tailwind-merge directly, compose it with defaultClassify() so reclassify still handles arrays and objects before Tailwind class conflict resolution runs:
import { configure, defaultClassify, type ClassValue } from "reclassify";
import { twMerge } from "tailwind-merge";
configure({
fn(value: ClassValue) {
return twMerge(defaultClassify(value));
},
});
// <div className={["px-2", "px-4", { "text-sm": false, "text-lg": true }]}>
// => <div className="px-4 text-lg">configure() changes reclassify's internal construction function, which is stored at the module level (app-wide mutable state), so call it during startup rather than per component:
- Client-side rendering / SPAs (e.g. default Vite): Call it in your main entry module before
render() - Server-side rendering (e.g. Next.js): Call it in the earliest server entry and earliest client entry that render JSX (e.g. root layout component)
If your custom function wants to build on the default behavior, you can import defaultClassify and use it:
import { configure, defaultClassify, type ClassValue } from "reclassify";
configure({
fn(value: ClassValue) {
const constructed = defaultClassify(value);
return constructed ? `custom ${constructed}` : "custom";
},
});If you want the same behavior in custom components, the underlying function can be imported via classify:
import { classify } from "reclassify";
classify(["btn", 42, { active: true, disabled: false }, ["nested"]]);
// => "btn 42 active nested"If a custom fn function is provided via configure(), the imported classify function points to that.
- Non-empty strings are kept as-is. Empty strings are dropped.
- Truthy numbers are stringified and kept.
- Arrays are flattened depth-first.
- Objects contribute keys whose values are truthy.
- Standalone falsy values like
false,0,"",null,undefined, andNaNare ignored.
This repository uses Vite+ (vp) on top of a pnpm workspace. Get started with Vite+ here.
The publishable library stays at the root, with example apps in apps/vite and apps/next.
Useful commands:
vp packbuilds the library package.vp testruns the library test suite.vp checkruns formatting, linting, and type-aware checks.vp run devruns the library watcher with the Vite example app.vp run dev:nextruns the library watcher with the Next.js example app.vp run build:vitetypechecks and builds the Vite example app.vp run build:nextbuilds the Next.js example app.vp run build:examplesbuilds both example apps.vp run checkruns the library validation plus both example app smoke tests.
Examples can be found in apps/:
apps/vite: The Vite app demonstrates intrinsicclassNamearrays and objects directly in JSX, plus a custom component that opts into the same pattern withclassify.apps/next: The Next.js app shows the same API through a framework setup usingjsxImportSource: "reclassify"intsconfig.json.
Both apps consume reclassify through the workspace package itself rather than importing source files from outside their own package directories.
If you're evaluating approaches to className construction, these tools are also worth knowing about.
Closest alternatives
reclsxandclsx-react: The closest runtime-level alternatives toreclassify.babel-plugin-transform-jsx-classnames: A compile-time approach that is conceptually close toreclassify.
reclassify's advantage in this group is that it pairs direct-in-JSX className authoring with intrinsic-element TypeScript support and a swappable app-wide construction function via configure().
Adjacent tools
clsxandclassnames: The most common manual helpers for conditionally constructingclassNamestrings.class-variance-authorityandtailwind-variants: Higher-level APIs for component variants and class composition.tailwind-merge: Useful alongsidereclassifywhen you want Tailwind conflict resolution.
Compared with manual helper libraries like clsx and classnames, reclassify lets intrinsic JSX elements accept array and object className values directly through a custom JSX runtime instead of requiring a helper call in each component.
MIT