Aug 27, 2023

When NOT to use shadcn/ui?

Discover when not to use shadcn/ui. Learn about alternative approaches for UI development. Make informed choices for your project's specific needs.
When NOT to use shadcn/ui?

TL;DR

shadcn/ui works best when you wish to build your UI component library while looking for a good starting point. This means heavy customizations on the components should (and in my opinion, MUST, more on that later) be expected. Suppose you want everything to work out of the box, including a predefined design system and apply relatively minor customizations to the built-in components, then a UI component library like MUI and Chakra UI may give you a better experience.

By the way, this website took the #2 approach and used Radix Themes.

How does shadcn/ui work differently from a UI component library?

Unlike UI component libraries, we are not installing shadcn-ui as a package and importing components from it, i.e., this is NOT how shadcn/ui works:

import { Button } from "shadcn-ui";

Instead, the components' source code is being generated by the CLI and added to the project. They become part of the project source code.

Project structure
your-project
├── components
│   ├── ui
│   │   ├── button.tsx
│   │   └── card.tsx
│   ├── your-folder
│   │   └── your-component.tsx
│   └── your-another-component.tsx
└── lib
    └── utils.ts

And then the components can be used by importing them from local files as if they are developed by you.

import { Button } from "@/components/ui/button";

At this point, shadcn/ui's mission is effectively completed. The generated code is now part of your source code. This approach is quite special and has its advantages and disadvantages.

Advantages

  • You have full control over the generated code and have the ability to apply any customizations you see fit.

  • If you are planning to implement your design system, doing so with shacdcn/ui as the base line is very easy. For example, for the Button component,

    button.tsx
    export const buttonVariants = cva(
      'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
      {
        variants: {
          variant: {
            default: 'bg-primary text-primary-foreground hover:bg-primary/90',
            destructive:
              'bg-destructive text-destructive-foreground hover:bg-destructive/90',
            outline:
              'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
            secondary:
              'bg-secondary text-secondary-foreground hover:bg-secondary/80',
            ghost: 'hover:bg-accent hover:text-accent-foreground',
            link: 'text-primary underline-offset-4 hover:underline',
          },
          size: {
            default: 'h-10 px-4 py-2',
            sm: 'h-9 rounded-md px-3',
            lg: 'h-11 rounded-md px-8',
            icon: 'h-10 w-10',
          },
        },
        defaultVariants: {
          variant: 'default',
          size: 'default',
        },
      },
    );
     
    export interface ButtonProps
      extends React.ButtonHTMLAttributes<HTMLButtonElement>,
        VariantProps<typeof buttonVariants> {
      asChild?: boolean;
    }
     
    export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
      ({ className, variant, size, asChild = false, ...props }, ref) => {
        const Comp = asChild ? Slot : 'button';
        return (
          <Comp
            className={cn(buttonVariants({ variant, size, className }))}
            ref={ref}
            {...props}
          />
        );
      },
    );
    Button.displayName = 'Button';

    All we need to do is to customize the Tailwind CSS classes in variants to customize the button without touching the rest of the code.

Disadvantages

  • The generated code is part of your source code now, something that you have to maintain and worry about, unlike a package where most of the logic is encapsulated.
  • The generated baseline is leaving a lot to be desired, and the users need to know what they are doing to effectively make use of it. This means heavy customization might be required from the very beginning, instead of an incremental process that some may assume. For example, some questionable designs in the Button component's API.
    1. icon is a size instead of being a dedicated prop like iconOnly or a dedicated component like IconButton
    2. destructive (also refers to as error or danger in other libraries) button is a variant instead of a color
    3. There is no success color out of the box. In fact, this is the case for the entire library.
  • If you prefer to maintain a connection between the customized components and shadcn-ui, patching will require a considerable amount of manual work, as opposed to just running npm update if it was a package.

Conclusion

Although shadcn/ui does simplify the UI creation process in a lot of cases while offering a high degree of customizability, hence the main reason it gets so popular, it is not the silver bullet. Instead, it is a specific tool targeting a very specific audience. It never fits the "this is the best component library" statement and is not beginner-friendly (beginner to FE development) at all. At the end of the day, it is about choosing the right tool for the right use cases.

I'm passionate about 🧠 innovative technologies, 😍 user experience web, and ♿️ accessibility.

Copyright © 2025 KWONG, Matthew Wang Shun