My Opinionated React Stack for 2026
Let me be upfront about something: this blog post was largely written by an AI agent. The starter kit it describes was extracted from a personal project by an AI agent. The components were scaffolded by AI agents. The code reviews were run by AI agents pretending to be Tanner Linsley and Dan Abramov.
I love this. I use AI to crank out project ideas, build tiny tools, see how things work in realtime. It's the best rapid prototyping environment I've ever had. But I've also watched enough vibe-coded projects collapse under their own weight to know that speed without structure is just fast debt.
The interesting question isn't whether to use AI for development. That ship sailed. The question is: what do you build on top of, so that when agents generate code, the code is consistent, explicit, and predictable?
That's what this starter kit is. Not a showcase of what AI can build — a foundation that keeps AI-generated code from turning into slop. A house style that serves humans and agents alike. I review every meaningful PR. I dip into the code for anything important. The agents handle volume; I handle direction.
After a few months of living with this stack for a personal project, I extracted the foundation into a reusable kit. Every opinion in this post was earned through actual usage — by me and my agents — not theoretical preference.
TanStack Start Over Next.js
I've shipped production Next.js. It's fine. The App Router is genuinely powerful. But somewhere around my third time debugging a cache invalidation issue across nested server components, I started asking myself: who is this complexity for?
TanStack Start is modular in a way Next.js isn't. The router, the query layer, the form library, the SSR framework — they're separate packages that compose cleanly. You can use TanStack Router without Start. You can use TanStack Query without either. The pieces don't assume each other, but they fit together with full type safety when you combine them.
The practical upside: for async data that flows with navigation — auth, user info, route-level queries — router context is a better home than React Context.
export const Route = createFileRoute('/dashboard')({
beforeLoad: ({ context }) => {
if (!context.auth.user) throw redirect({ to: '/login' })
},
loader: ({ context }) =>
context.queryClient.ensureQueryData(dashboardQueryOptions()),
})The more you keep at the router level, the easier it is to orchestrate loader waterfalls, parallelize data fetching, and reason about the SSR boundary. You stop scattering useEffect calls across components just to coordinate data loading.
To be clear: React Context still owns what it's good at — synchronous, tree-local state like theming, locale, and UI flags. Router context isn't a replacement for it. It's a better primitive for a specific class of problems that often gets shoved into Context by default.
There's an agent benefit here too: TanStack's file-based routing with typed params means an agent scaffolding a new route gets compile-time feedback on whether it wired things correctly. With implicit routing conventions, agents guess. With explicit typed contracts, they know.
The tradeoff: TanStack Start is younger. Early on, package versions were loosely coupled and things broke between releases. I ate that pain. The core ergonomics were worth it.
Panda CSS: Composition Over Concatenation
Tailwind is great at what it does. The ecosystem is massive, the tooling is solid, and it's battle-tested at scale. I'm not here to tell you Tailwind is bad.
But Tailwind's model is fundamentally concatenation — you build styles by assembling strings. Panda CSS's model is composition — you build styles by composing typed objects.
const button = cva({
base: {
display: 'inline-flex',
alignItems: 'center',
borderRadius: 'md',
fontWeight: 'semibold',
cursor: 'pointer',
},
variants: {
intent: {
primary: { bg: 'brand.500', color: 'white', _hover: { bg: 'brand.600' } },
ghost: { bg: 'transparent', color: 'text.primary', _hover: { bg: 'bg.subtle' } },
},
size: {
sm: { px: '3', py: '1.5', fontSize: 'sm' },
md: { px: '4', py: '2', fontSize: 'md' },
},
},
})That recipe is a function. It's typed. If you typo a token, TypeScript catches it at compile time — in neovim, in VS Code, in CI — not when a user sees a broken button in production. And because Panda tokens are values — not strings — you can reference them in template strings, spread them across objects, merge style objects, and write functions that transform recipes. These are things you can't do when styles live in class name strings, no matter how good the editor tooling is.
This is also why I chose css() and cva() only — no styled() API. One pattern means one mental model. When I tell an agent "use cva() for variants," it does. When there are three valid approaches, agents pick whichever one was most common in their training data. Fewer valid approaches means fewer arguments in code review and fewer hallucinated alternatives from agents.
The z-index Rule
Every z-index value in this repo is -1, 0, or 1. That's it. Three values.
If you're unfamiliar: the browser doesn't just stack elements by their z-index number globally. Certain CSS properties (transform, opacity < 1, position: fixed, will-change, and others) create stacking contexts — isolated z-index namespaces where children are stacked relative to each other, not the rest of the page. Understanding this is the key to understanding why you almost never need z-index: 9999.
z-index: -1; /* Behind siblings */
z-index: 0; /* Default / create stacking context */
z-index: 1; /* Above siblings */If you need z-index: 50 for a modal, your DOM structure is probably wrong. A modal should be a portal rendered at the end of the body. A dropdown should create its own stacking context. The CSS spec already solved these problems — we just keep re-solving them with bigger numbers.
Do exceptions exist? Yes — third-party widgets, iframes, and browser autofill overlays sometimes force your hand. But I'd rather document those exceptions explicitly than open the floodgates to arbitrary values. Every exception requires a comment explaining why. In practice, I've needed exactly zero so far.
This rule is also a great litmus test for agents. An agent that generates z-index: 50 is an agent that doesn't understand the codebase. With the constraint documented in CLAUDE.md, they don't.
The Field Pattern
Components are where opinions get expensive. Here's one I'm happy with:
import * as ui from '@/components/ui'
// Composed — full control over layout
<ui.Field.Root>
<ui.Field.Label>Email</ui.Field.Label>
<ui.Field.Input type="email" placeholder="you@example.com" />
<ui.Field.Error>Please enter a valid email</ui.Field.Error>
</ui.Field.Root>
// Convenience wrapper — the 80% case
<ui.Input label="Email" type="email" error={errors.email} />Both exist. The composed version gives you full layout control. The wrapper handles common cases. Neither is "right" — they serve different needs within the same codebase.
Note the namespace import: import * as ui from '@/components/ui'. Every component comes through one namespace. No hunting for imports, no wondering if it's @/components/Button or @/components/ui/Button. It's always ui.Button. An agent never has to guess.
No More forwardRef
React 19 lets you pass refs as regular props. forwardRef still exists — it's deprecated, not removed — but there's no reason to reach for it in a new codebase. This repo has zero forwardRef calls by choice.
// Before: forwardRef gymnastics
const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => (
<input ref={ref} {...props} />
))
// After: it's just a prop
function Input({ ref, ...props }: InputProps) {
return <input ref={ref} {...props} />
}Less API surface, better type inference, simpler mental model. Agents trained on React 18 code will still reach for forwardRef unless you tell them not to. So I tell them not to.
Cloudflare Workers
Cost times complexity. Both low. That's the whole pitch.
Workers run at the edge, cold starts are measured in single-digit milliseconds, and the platform keeps adding capabilities. Deploying is wrangler deploy. The bill for a hobby project is effectively zero.
I'm not claiming Workers are right for everything. But for a starter kit that might power anything from a personal site to a SaaS MVP, the economics and simplicity are hard to beat. And wrangler deploy is one command an agent can run without a deployment tutorial in the system prompt.
Your CLAUDE.md Is Documentation
Here's the part most starter kits miss, and the real thesis of this post.
This kit ships with a CLAUDE.md — not as an afterthought, but as first-class documentation. Architecture decisions, component creation guides, anti-patterns, naming conventions. It's the document a senior engineer would write for a new team member, except the new team member might be Claude, or Copilot, or whatever comes next.
There are four agent skills baked in for scaffolding components, routes, stories, and recipes. When an agent needs a new component, the skill knows the patterns — cva() not styled(), namespace exports, the Field composition approach, the testing conventions. The agent doesn't have to guess, and I don't have to correct.
Every architectural rule in this post — the z-index constraint, the css()/cva() restriction, the namespace imports — doubles as an instruction for a coding agent. When your constraints are explicit and documented, agents follow them. When they're tribal knowledge in a senior engineer's head, agents hallucinate something else.
Most vibe-coded projects fail not because the AI is bad at writing code, but because there's no definition of what good code looks like for this specific project. A starter kit with documented conventions solves that. The AI handles volume. You handle taste.
What's Not Here
No auth. No database. No payments. No CMS.
Those are application-level decisions, not infrastructure decisions. This kit is the layer below that — the rendering, styling, routing, and deployment foundation you build features on top of. Adding Clerk or Supabase or Stripe is straightforward. Baking them in would be opinionated in the wrong direction.
That said, the repo ships with recipes/ — auth, forms, analytics, Convex, Storybook deployment, and more — as drop-in additions the agent's skills are aware of, so it can adopt them as-is or adjust them to your needs.
Ship It
The repo is open. Fork it, rip out what you don't like, keep what you do. Let the agents loose on it. Then review what they wrote.
If you think the z-index rule is naive, open an issue and show me a case three values can't handle. If you think I'm wrong about BaseUI or Panda CSS, I'd genuinely like to hear why. If you think TanStack Start isn't ready — it wasn't a year ago. It is now.
The Stack (Reference)
| Layer | Choice | Why |
|---|---|---|
| Runtime | Bun | Fast, batteries-included, TypeScript-native |
| Framework | TanStack Start | Modular, type-safe, no vendor lock-in |
| UI Library | React 19 | Ref-as-prop, no forwardRef, you know the rest |
| Styling | Panda CSS | Composable typed tokens, near-zero runtime |
| Components | BaseUI | Accessible headless primitives from the Radix team |
| Testing | Vitest + Playwright | Fast unit tests, real browser E2E |
| Stories | Storybook 10 | Component development in isolation |
| Deployment | Cloudflare Workers | Edge SSR, single-digit ms cold starts |
| Env Validation | Varlock | Type-safe .env with schema decorators |
I'm Aidan. I build things on the web. Occasionally I have opinions about it. More at outsidetheboxmodel.com.