flowingkhaosSign inBrowse the marketplace
New here? flowingkhaos is a build-in-public workshop. This is its weekly column.What this site is

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

React is a per-page cost: the three React apps keep 77 shadcn components, new sites go tokens-first with one CSS token sheet and Preact islands.

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 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 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 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, and I already run both: one package on Radix, one on Base UI. Aceternity’s guide to 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 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.

Island weight against scene weight on lukesidney.me
3D world island
1.1 KB gzipped
Scene chunk
230 KB gzipped

Source: 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 documents. A busy month is when quiet failures hide best.

Business CSS before and after the Tailwind fix
Before fix
106 KB
After fix
167 KB

Source: 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.

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 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 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 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. And if your page really needs React, the shadcn CLI docs cover how to start, and I’d still recommend it there.

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

Sources

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.

Keep reading

The column · weekly · free

The next piece, the morning it is published.

One written column a week. The email service holds the list.

The columns, fixes and workshops continue in the community: SEO, AI, and making money with code.

Join on Whop

Luke Sidney writes and builds everything here, alone, in the open. lukesidney.me