shadcn-ui / ui

Beautifully designed components that you can copy and paste into your apps. Accessible. Customizable. Open Source.
https://ui.shadcn.com
MIT License
63.35k stars 3.57k forks source link

Examine usage of tw-classed instead of cva #152

Open jondot opened 1 year ago

jondot commented 1 year ago

Hi, Great repo! I've been doing something similar, but not as extensive, and considering throwing away my stuff and adopting yours. Once thing I've been doing differently is using tw-classed, so a component looks like this:

import { classed } from '@tw-classed/react'

export const Input = classed.input({
  variants: {
    v: {
      normal:
        'appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500',

      sm: 'appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 text-sm',
    },
  },
  defaultVariants: {
    v: 'normal',
  },
})

Which is very similar to the intent of your usage of cva but without all the forwardRef, and manually creating props etc. It could cut the code for this repo by at least 3x.

Did you ever consider this?

shadcn commented 1 year ago

but without all the forwardRef, and manually creating props etc

Interesting. Could you share an example for this? Wondering how this works.

jondot commented 1 year ago

Yea, it's actually exactly the button example from above:

export const Button = classed.button({
  variants: {
    v: {
      primary:
        'inline-flex justify-center items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500',
      secondary:
        'inline-flex justify-center items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500',
      disabled:
        'opacity-50 cursor-not-allowed inline-flex justify-center items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500',
    },
  },
  defaultVariants: {
    v: 'secondary',
  },
})

And this is how my shadcn/ui based button looks like:

export const Button = classed.button(
  'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:hover:bg-slate-800 dark:hover:text-slate-100 disabled:opacity-50 dark:focus:ring-slate-400 disabled:pointer-events-none dark:focus:ring-offset-slate-900 data-[state=open]:bg-slate-100 dark:data-[state=open]:bg-slate-800',
  {
    variants: {
      variant: {
        filled:
          'bg-primary-600 text-white hover:bg-primary-500 dark:bg-slate-50 dark:text-slate-900',
        destructive:
          'bg-red-500 text-white hover:bg-red-600 dark:hover:bg-red-600',
        outline:
          'shadow-sm bg-transparent text-slate-900 border border-slate-300 hover:bg-slate-100 dark:border-slate-700 dark:text-slate-100',
        subtle:
          'bg-slate-100 text-slate-900 hover:bg-slate-200 dark:bg-slate-700 dark:text-slate-100',
        ghost:
          'bg-transparent hover:bg-slate-100 dark:hover:bg-slate-800 dark:text-slate-100 dark:hover:text-slate-100 data-[state=open]:bg-transparent dark:data-[state=open]:bg-transparent',
        link: 'bg-transparent dark:bg-transparent underline-offset-4 hover:underline text-slate-900 dark:text-slate-100 hover:bg-transparent dark:hover:bg-transparent',
      },
      size: {
        default: 'h-10 py-2 px-4',
        sm: 'h-9 px-2 rounded-md',
        lg: 'h-11 px-8 rounded-md',
      },
    },
    defaultVariants: {
      variant: 'outline',
      size: 'default',
    },
  }
)
export type ButtonProps = Classed.ComponentProps<typeof Button>

The author clearly built a cva alternative, so the interface is very compatible. I just copy-paste the values from shadcn/ui into a classed.button and everything works.

The infrastructure is hidden under the classed library, typing works because they use plenty of advanced Typescript inference mechanisms.

mikebuilds commented 1 year ago

tw-classed looks pretty cool…

As far as I can see, it doesn’t support twMerge - how could this fit in?

riordanpawley commented 1 year ago

tw-classed looks pretty cool…

As far as I can see, it doesn’t support twMerge - how could this fit in?

It does if you use

const classed = createClassed({ merger: twMerge });

AnandChowdhary commented 1 year ago

@shadcn maybe a migration from cva to tw-classed would be beneficial sooner rather than later, because it will likely result in a large diff for all components since a lot of boilerplate can be eliminated. Is this something on your radar already, and if not, would you be open to a PR?

kenbankspeng commented 11 months ago

I've replaced cva with my own reducer function but am interested in tw-classed. But given tw-classed is bringing css-in-js into the tailwind paradigm, is there any concern with it playing well with react server components (I think emotion/styled-components don't play so well)? Or do we see in this scenario that tw-classed will only be used with interactive client side components anyway, so not an issue?

See https://github.com/reactwg/react-18/discussions/110 and https://dev.to/srmagura/why-were-breaking-up-wiht-css-in-js-4g9b

Akkuma commented 4 months ago

This is an older issue, but there is also https://react-twc.vercel.app/ (https://github.com/gregberge/twc) which is pretty similar