React vs shadcn, and trying to be too clever
Jun 30, 2025
Ayush, Autumn Co-Founder

You know those moments where you have an idea so brilliant you feel like Steve Jobs? Launching our component library felt like that. Novel, high risk, and a radically different approach to what was on the market. If it worked out, it'd be incredible.
Unfortunately, it didn't. We ended up with a weird mess and had to spend the last week rewriting everything.
For context: we're building Autumn, pricing and billing software. Part of our offering is frontend components for things like a pricing table, paywalls etc. You can read more about them here.
V1 as a React library
We first attempted this as a standard npm React library:
It's biggest issue was customisability. There are two ways to make npm components customisable: through props, or a no-code interface in our app.
With props there were too many class variables. For example with the pricing page, each pricing card is composed of many headers, descriptions, buttons and more. And each card itself can have variants (recommended, discounts, annual etc).
A low code interface is a bad experience for devs, who want full customisability and have 10x experience designing with tailwind/css. Even more so with the era of cursor. This is subjective but I’ve never had a good experience with one.
Sidenote: If you ever try to make your npm component customisable via tailwind, all the best. That was one of the most frustrating days of our lives.
Also, If you ever forgot what jsx styles look like, here’s a snippet of what we had to write…

We made one measly post on linkedin about it, decided it was unusable and shelved it.
V2 as shadcn/ui components
A few weeks later we started looking into it again with shadcn.
This immediately felt like a more customizable and fluid DX, but had its own challenges:
Because shadcn components install as a user file, we cannot fully control it via our SDK. For example, if we wanted to trigger a modal/popup to confirm a plan upgrade, the user needs to explicitly pass it into a function. This does take away slightly from that “magical DX”.
Since users own and control the component files, deciding what data abstraction level to return to the frontend is hard. For example, our upgrade dialog shows different text and styling depending on the scenarios:
- One time purchase vs subscription
- Upgrades vs downgrades vs cancellations vs renewals
- Does the upgrade require an input from users (eg, quantity of credits to purchase)

With a React library, everything would be "completely processed", with no customizability. Initially this is the same approach we took with the shadcn registry, but users quickly told us they wanted to customize the text too. We switched to the "in-between" approach.
Our API will return a scenario (eg, upgrade, downgrade, add-on, free-trial etc), and our shadcn components install with a library of cases that users can edit to control the messaging.

However, the shadcn library turned out as a huge mess for several reasons:
Mistake 1
We decided to launch our components as a shadcn package instead of plain React. Our components approach was inspired by Clerk, but I always thought they looked out of place and wanted users to own them. Shadcn had launched registry functionality so people could share and download components, and Supabase launched one too, so it seemed promising.
Unfortunately only half our users use shadcn, but the others wanted components and couldn't use them. More importantly, we were iterating quickly on the underlying API that controls the component content. Since the components weren't synced to our SDK, every update we made broke integrations. We should have kept it simple with React to start, then expanded to shadcn once stable.

Mistake 2
We launched a separate open source pricing component library called pricecn. We thought it would go viral like React Email by Resend. We designed it so pricecn users could easily migrate to using Autumn components later later, and so made the Autumn component library have the pricecn library as a dependency.
Pricecn didn't take off. People liked generating pricing cards from JSON, but it's hard enough to promote one project, let alone two. Having Autumn components depend on pricecn ones was just a confusing experience for users, as each component came with several files in different folders.

Mistake 3
We launched with 3 different styles: classic, clean and dev. I thought this would grow adoption since people could pick what suits them.
Again, people liked the concept but it made maintenance hell. Every issue meant jumping into 3 component files, so we kept putting off fixes until it became unusable.

What we have now
We launched a React library as the core experience, but allowing users to download the same files as shadcn components to customize it
We only kept the "classic" style and simplified it.
We cut the dependency on pricecn.
We kept the shadcn components since people liked owning them when they worked, and now our API is a little more stable, it's working better.
Maybe we're still trying too hard to be clever but here's the cool part: instead of maintaining two sets, our shadcn library automatically replicates from our React ones so they always stay synced. Here's the script that syncs them whenever we npm run dev
:
https://github.com/useautumn/autumn-js/blob/main/package/scripts/sync-registry.ts

To be honest we have no idea how that script works but it does. Thanks Claude Code!