# shadcn Alternative for Content Sites: Drop React, Keep the Tokens

By Luke Sidney (https://lukesidney.me) · 21 Oct 2026 · updated 11 Nov 2026
Source: https://flowingkhaos.com/column/moving-away-from-shadcn

> A shadcn alternative for content sites: drop the React layer, keep one CSS token sheet with plain components and Preact islands only where pages move.

## The Short Answer: Tokens First for New Work, React Only Where a Page Moves

I'll say it straight: for anything new, my shadcn alternative is no component library at all. One CSS token sheet, plain components, and Preact islands only where a page actually moves. A content page that ships no React has no React bill to justify, and I wrote the reversal number down before I'd need it.

### My shadcn alternative, as a rule

That is a rule now, not a mood. On 2026-10-21 I changed my own stack rule from "Astro with React islands" to tokens-first for new sites and public pages. The old rule said "Never Fresh, while our component library is React." I had already shipped a Fresh site anyway, and the rule had stopped describing what I do.

What I'm not doing: rewriting anything. I run 77 shadcn components across two packages and three React apps, and they stay. Those apps are dense, interactive tools where React earns its place every second.

I also owe shadcn some credit. The [shadcn/ui docs](https://ui.shadcn.com/docs) describe components you copy into your own codebase instead of installing a black box, and that copy-in model is exactly why I could restyle all 77 through my own tokens. The tooling keeps moving too: the [shadcn CLI v4 changelog of March 2026](https://ui.shadcn.com/docs/changelog/2026-03-cli-v4) documents one of several releases this year. None of this is a complaint about the library's quality.

And one more thing, said once and early. I believe AI writes better UI against a token sheet than against a React component library, because the model writes plain HTML and CSS that read the tokens, with no client framework in the way. This is my belief, not a measurement. I'll tell you at the end what test would settle it.

If you want the longer story of how I ended up on Fresh in the first place, I explain it in [my thoughts on Fresh](https://lukesidney.me/blog/my-thoughts-on-fresh) on my personal site.

## Where shadcn Sits in My Stack Today, and What Changed Under It

Here is what I counted on 2026-10-20:

| Package or app | Size | Base |
|---|---|---|
| `@kos/ui-radix` | 38 components, 19 `@radix-ui/*` dependencies | Radix, React 19 |
| `@kos/ui-baseui` | 39 components | Base UI, React 19 |
| OSI | 114 files import the package | its own shadcn config |
| Business | 61 files import the package | its own shadcn config, different style |
| bookings | 17 files import the package | its own shadcn config, different style |

I never used shadcn vanilla. On 2026-08-24 I wrote down why I took it at all: for the light Radix structure, then restyled everything through my own tokens. Those tokens live in one shared theme, a `base.css` of 9,297 bytes and a `kit.css` of 17,031 bytes. The UI revamp of 2026-10-03 put that one token set over four apps that had four palettes and four font stacks.

Look at that sentence again. The thing that unified my apps was the token sheet, not the component library. The components just read it. One of my apps, ian, already runs with no shared React packages at all: it copies the tokens and nothing else.

The ground under shadcn has shifted this year too. The [July 2026 changelog made Base UI the default](https://ui.shadcn.com/docs/changelog/2026-07-base-ui-default), and I already run both: one package on Radix, one on Base UI. Aceternity's [guide to shadcn/ui alternatives](https://ui.aceternity.com/blog/shadcn-ui-alternatives) covers the options that grew around it, and notes that Magic UI describes itself as a companion to shadcn/ui rather than a replacement. That is the pattern of the whole market: more layers on the same base.

I disagree with leaving over a default flip. A new default is a reason to check your foundation, not to abandon it. My reason is different, and it is about the pages that never needed React in the first place.

## Does a Content Page Need React at All? The Measured Islands

I have two sites that already answer that question with no React in them.

| Site | Framework | Token sheet | Islands | Island JS |
|---|---|---|---|---|
| lukesidney.me | Fresh 2.3 + Preact, plain CSS | 99 lines, 4,452 bytes | 4 | not measured as a total |
| flowingkhaos.com | Astro 7.3.2 + Preact | 139 lines, 6,804 bytes | 4 | about 37 KB raw, local build, not production |

lukesidney.me goes further than not using React: its architecture, written on 2026-09-02, bans React, Tailwind, shadcn and my shared UI packages, and a grep gate enforces the ban. flowingkhaos.com, the site you're reading, has four Preact islands: breadcrumbs, a check, a rail and a tag filter.

The flowingkhaos number needs its caveats attached. About 37 KB raw is every island script together in a local build on 2026-10-20, of which the Preact runtime is 10.5 KB and signals 7.9 KB. It is not measured in production, and I have no gzip figure. So it is a size, not a performance claim.

The lukesidney record separates two costs I think people mix up. Its 3D world island is 1.1 KB gzipped. The scene it loads is 230 KB gzipped. LCP, the time until the biggest thing on screen is painted, was unchanged. The heavy part is the scene, not the island wrapper. The framework around an island can be almost nothing.

What I still owe: the per-page gzipped JavaScript for flowingkhaos.com against one page of OSI, my React app. Until I have it, "more performant" is a direction, not a number.

For contrast, my [Travole workshop's stack page](https://flowingkhaos.com/workshops/deploy-your-own-travole/stack-and-dependencies) lists a full Next.js and React app, dependency by dependency. That is the right tool there. An app with sessions, forms and live state is a different job from a page of text.

Solid, Fresh, Astro and even Angular look appealing to me again now that the component layer isn't tied to React. I've shipped Fresh and Astro. I have not evaluated Solid or Angular, so I won't rank them.

```chart
{"title":"Island weight against scene weight on lukesidney.me","unit":" KB gzipped","data":[{"label":"3D world island","value":1.1},{"label":"Scene chunk","value":230,"highlight":true}],"source":{"label":"Our own measurements"}}
```

## What the React Layer Cost Me, Dated

I count none of these as disasters. All of them were quiet, which is worse.

| Date | What broke | Cause in the record |
| --- | --- | --- |
| 2026-08-14 | A design lock never reached app code; the dashboard kept stock shadcn cards | Delivery gap |
| 2026-08-17 | Business shipped with no app shell at all | Tailwind v4 never compiled classes in the shared UI package (missing @source line); a v3-style class in the sidebar produced CSS the browser silently dropped |
| 2026-10-06 | Type check broke after a folder rename | A JSX package with no React types of its own |

On 2026-08-14, a design lock never reached the app code. The dashboard kept stock shadcn cards anyway. The design lived next to the components instead of inside them.

On 2026-08-17, my Business app shipped with no app shell at all. Tailwind v4 never compiled the classes inside my shared UI package, because one `@source` line was missing. On top of that, a v3-style class in the sidebar produced CSS the browser silently dropped. Business CSS went from 106 KB to 167 KB once it was fixed, which tells you how much styling had simply not been there. The lesson: a component library that lives in another package can fail without a single error.

On 2026-10-06, a JSX package with no React types of its own broke the type check after a folder rename. A type boundary between packages, the kind of problem that only exists because the components are React components shared across apps.

Then there was a token-name collision. Copying a design lock straight into an app turned every hover state tangerine, because the lock's token names collided with shadcn's. That one is half a React-layer problem and half a token problem, which is a good place to turn the page.

Each of these came from the same shape: a shared React component package between the design and the page. They also landed in the same weeks I was measuring why my CMS had to leave Vercel, a move the fix [Vercel Hobby could not carry the CMS](https://flowingkhaos.com/fixes/vercel-hobby-could-not-carry-the-cms) documents. A busy month is when quiet failures hide best.

```chart
{"title":"Business CSS before and after the Tailwind fix","unit":" KB","data":[{"label":"Before fix","value":106},{"label":"After fix","value":167,"highlight":true}],"source":{"label":"Our own measurements"}}
```

## What Tokens Broke Too: The Honest Failure Log

I'd be selling you something if I stopped there. Tokens broke things too.

| Date | What broke | Cause |
|---|---|---|
| 2026-09-27 and 2026-10-14 | clevrstart's `:root` font variables were empty, twice | a plain CSS-variable scoping bug |
| undated | Astro's scoped styles outranked global breakpoints on flowingkhaos.com | style scoping |
| undated | copying a design lock turned every hover state tangerine | token names colliding with shadcn's |

The clevrstart one stings, because it happened twice. A token sheet is just CSS custom properties, and custom properties follow scoping rules. Define them on the wrong selector and the page reads empty values with no warning. Same quiet failure as the Tailwind one, different layer.

The Astro one is the framework being clever. Scoped styles are a feature, right up until they beat the breakpoint you set globally. Moving to Astro on Cloudflare had other side effects as well, like the trailing slash that [left duplicate URLs in my sitemap, which I documented as a fix](https://flowingkhaos.com/fixes/astro-trailing-slash-cloudflare-sitemap-duplicates).

So a token sheet is not free. It moves the failures from "a package didn't compile" to "a variable didn't resolve". I think the second kind is easier to find, because it lives in one file you can read top to bottom. That is an opinion, not a measurement.

And tokens don't cure sameness on their own. If you take someone else's token sheet, you get someone else's site. The value is in writing your own, and that takes work.

## The Sameness Problem: What Outside Voices Get Right and Where I Push Back

Tailgrids opens its [list of shadcn alternatives](https://tailgrids.com/blog/shadcn-alternatives) with the line I hear most: as more developers rely on the same stacks, patterns and AI-generated layouts, many interfaces are starting to look identical. I agree with the observation. I don't think it is proven, and I'm not going to prove it here. It is the claim my AI belief would have to survive.

Untitled UI's [comparison with shadcn/ui](https://www.untitledui.com/blog/untitled-ui-vs-shadcn) argues "add, don't switch, if the problem is coverage", which is right for a React app, and my problem is not coverage: it is a content page paying a React runtime it never used.

Watermelon's [guide to free shadcn alternatives](https://ui.watermelon.sh/guides/best-free-shadcn-alternatives) recommends daisyUI for CSS-only styling that works outside React, the closest mainstream neighbor to a token sheet; most other options keep React as the floor.

So here is what the outside voices get right. Sameness is real when everyone starts from the same registry and the same AI layouts. Coverage gaps in a React app are better fixed by adding components than by switching. And CSS-only styling outside React is already a mainstream option, not a fringe one.

Where I push back on all of them: they treat React as the floor and argue about what sits on top. I'm moving the floor for one kind of page, the content page, and keeping it where it is for the apps.

## The Reversal Number, the Rule Amendment, and What Is Still Unproven

Here is the exit I wrote before I needed it.

On 2026-08-30 my stack rule said "Never Fresh, while our component library is React" and defaulted new sites to Astro with React islands. On 2026-10-21 I changed it: new sites and public pages are tokens-first, with Preact islands by default, and Solid, Fresh and Astro all allowed. React enters a page only for a component that exists nowhere else, on that island alone. The three React apps keep shadcn.

> **The reversal trigger.** A tokens-first site goes back to Astro with React islands if either happens: it re-implements more than 10 components that already exist in my shared packages, or its client JavaScript passes 60 KB gzipped per page on two measured releases.

The reversal trigger is a number. If a tokens-first site re-implements more than 10 components that already exist in my shared packages, or its client JavaScript passes 60 KB gzipped per page on two measured releases, that site goes back to Astro with React islands.

What is still unproven, plainly. The AI belief is not measured; the test that would settle it is the same component brief generated twice, once against a token sheet and once against my shadcn packages, with the output compared side by side. There is no gzipped per-page figure for flowingkhaos.com yet. There are no field Core Web Vitals for any of my sites, and no bundle size for the React apps. I have not evaluated Solid or Angular.

So this is a stance with receipts, not a benchmark. If you want the Fresh side of the story, I explain it in [my thoughts on Fresh](https://lukesidney.me/blog/my-thoughts-on-fresh). And if your page really needs React, the [shadcn CLI docs](https://ui.shadcn.com/docs/cli) cover how to start, and I'd still recommend it there.

React is a per-page cost. Make it justify itself.

## Sources

- [Introduction - shadcn/ui](https://ui.shadcn.com/docs)
- [March 2026 - shadcn/cli v4 - shadcn/ui](https://ui.shadcn.com/docs/changelog/2026-03-cli-v4)
- [shadcn - shadcn/ui](https://ui.shadcn.com/docs/cli)
- [July 2026 - Base UI as the Default - shadcn/ui](https://ui.shadcn.com/docs/changelog/2026-07-base-ui-default)
- [Best Free shadcn/ui Alternatives in 2026 | Watermelon UI](https://ui.watermelon.sh/guides/best-free-shadcn-alternatives)
- [11+ Best Shadcn Alternatives for Building React/Next.js UIs | Tailgrids UI](https://tailgrids.com/blog/shadcn-alternatives)
- [shadcn/ui alternatives, and how to tell which one you actually need | Aceternity UI Blog](https://ui.aceternity.com/blog/shadcn-ui-alternatives)
- [Untitled UI vs. shadcn/ui: Which React Component Library Should You Use in 2026? | Untitled UI](https://www.untitledui.com/blog/untitled-ui-vs-shadcn)
- [The stack and every dependency — Deploy your own Travole](https://flowingkhaos.com/workshops/deploy-your-own-travole/stack-and-dependencies)
- [Vercel Hobby could not carry the CMS — flowingkhaos](https://flowingkhaos.com/fixes/vercel-hobby-could-not-carry-the-cms)
- [Astro trailing slash on Cloudflare: fix the 307 and sitemap duplicates — flowingkhaos](https://flowingkhaos.com/fixes/astro-trailing-slash-cloudflare-sitemap-duplicates)

## Questions, answered

### What shadcn alternative does this article recommend?

For content pages, the author uses a single CSS token sheet with plain components, and Preact islands only where a page actually moves. The existing shadcn setup stays in the three React apps.

### When would the author go back to shadcn?

A tokens-first site reverts to Astro with React islands if it re-implements more than 10 components that already exist in the shared packages, or if its client JavaScript passes 60 KB gzipped per page on two measured releases.

### Does every page still need React with this approach?

No. Two content sites already run with no React in them, using Fresh and Astro with Preact islands. React enters a page only for a component that exists nowhere else, on that island alone.
