Skip to content

Custom components don't accept ClassValue in className — only intrinsic elements get widened types #1

Description

@andriy-panchiy

Custom components don't accept ClassValue in className — only intrinsic elements get widened types

Problem

reclassify widens className types for intrinsic HTML elements via JSX.IntrinsicElements, but custom React components still expect className: string. This means passing arrays/objects to any custom component is a TypeScript error, even when it works perfectly at runtime.

This is especially painful with component libraries like shadcn/ui + Radix UI, where wrapper components derive their props from React.ComponentProps<typeof RadixPrimitive.Something> or React.ComponentProps<"button">. These resolve through React's own HTMLAttributes/ButtonHTMLAttributes where className is still string.

Example

// ✅ Works — intrinsic element, reclassify widens the type
<div className={['px-2', isActive && 'bg-blue-500']} />

// ❌ TypeScript error — Button is a custom component
// Type '(string | false)[]' is not assignable to type 'string'
<Button className={['px-2', isActive && 'bg-blue-500']} />

Where Button is a typical shadcn component:

function Button({ className, ...props }: React.ComponentProps<"button">) {
  return <button className={cn(buttonVariants(), className)} {...props} />
}

At runtime this works fine because cn() (which uses classify()) handles arrays. But TypeScript rejects it.

Current workaround

We had to manually widen className in two places:

  1. React module augmentation for standard HTML attribute interfaces:
declare module 'react' {
  interface HTMLAttributes<T> { className?: ClassValue }
  interface ButtonHTMLAttributes<T> { className?: ClassValue }
  interface InputHTMLAttributes<T> { className?: ClassValue }
  // ... 10+ more interfaces
}
  1. Changing each shadcn component's props individually:
// Before
function Badge({ className, ...props }: React.ComponentProps<"span">) {

// After
function Badge({ className, ...props }: Omit<React.ComponentProps<"span">, 'className'> & { className?: ClassValue }) {

This is tedious and doesn't scale — every new shadcn component or Radix primitive wrapper needs the same treatment.

Suggestion

reclassify could ship a global type augmentation (opt-in or default) that widens className on React's base HTMLAttributes<T> and SVGAttributes<T>. Since reclassify already widens JSX.IntrinsicElements, extending the underlying React attribute interfaces would make the types consistent for custom components that derive their props from React.ComponentProps<"element">.

Something like a reclassify/types/global entry point:

// In user's project, or auto-included by reclassify:
import type { ClassValue } from 'reclassify';

declare module 'react' {
  interface HTMLAttributes<T> {
    className?: ClassValue;
  }
  interface SVGAttributes<T> {
    className?: ClassValue;
  }
}

This alone would fix ~80% of cases. The remaining Radix-specific props would still need individual fixes, but that's a much smaller surface.

Environment

  • reclassify: latest
  • React: 19.2.3
  • Next.js: 16.1.0
  • TypeScript: 5.x
  • shadcn/ui + Radix UI

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions