Skip to main content

Tanstack Start Shadcn Tailwind

Guidelines for building TanStack Start applications

You are an expert TypeScript developer specializing in TanStack Start applications with modern React patterns.

Apply these rules in projects using @tanstack/react-start. Read installed Start, Router, Query, and Tailwind versions and the existing build configuration before changing APIs or dependencies.

Framework Configuration

  • Follow the project's existing supported build integration. In Vite projects, import tanstackStart from @tanstack/react-start/plugin/vite and place it before the React Vite plugin; do not copy obsolete configuration or migrate an existing app as a side effect.
  • Let the routing integration generate routeTree.gen.ts; edit route source files instead of the generated tree.
  • Configure error and pending UI through router defaults or route-level errorComponent and pendingComponent options.
  • Match Tailwind setup to its installed major version: v4 uses CSS-first configuration and the appropriate build plugin; retain a v3 app's configuration until an explicit migration.

References: Start build configuration, route options, Tailwind upgrade guide.

Tech Stack

  • TypeScript (strict mode)
  • TanStack Start (routing & SSR)
  • Shadcn/ui (UI components)
  • Tailwind CSS (styling)
  • Zod (validation)
  • TanStack Query (client state)

Code Style Rules

  • NEVER use any type - always use proper TypeScript types
  • Prefer function components over class components
  • Always validate external data with Zod schemas
  • Provide error and pending UI through router defaults or route overrides
  • Follow accessibility best practices with ARIA attributes

Component Patterns

Use function components with proper TypeScript interfaces:

interface ButtonProps {
  children: React.ReactNode;
  onClick: () => void;
  variant?: 'primary' | 'secondary';
}

export default function Button({ children, onClick, variant = 'primary' }: ButtonProps) {
  return (
    <button onClick={onClick} className={cn(buttonVariants({ variant }))}>
      {children}
    </button>
  );
}

Data Fetching

Use Route Loaders for:

  • Initial page data required for rendering
  • SSR requirements
  • SEO-critical data

Use React Query for:

  • Frequently updating data
  • Optional/secondary data
  • Client mutations with optimistic updates
// Route Loader
export const Route = createFileRoute('/users')({
  loader: async () => {
    const users = await fetchUsers()
    return { users: userListSchema.parse(users) }
  },
  component: UserList,
})

// React Query
const { data: stats } = useQuery({
  queryKey: ['user-stats', userId],
  queryFn: () => fetchUserStats(userId),
  refetchInterval: 30000,
});

Zod Validation

Always validate external data. Define schemas in src/lib/schemas.ts:

export const userSchema = z.object({
  id: z.string(),
  name: z.string().min(1).max(100),
  email: z.string().email().optional(),
  role: z.enum(['admin', 'user']).default('user'),
})

export type User = z.infer<typeof userSchema>

// Safe parsing
const result = userSchema.safeParse(data)
if (!result.success) {
  console.error('Validation failed:', result.error.format())
  return null
}

Routes

Structure routes in src/routes/ with file-based routing. Override the router's error and pending UI when the route needs different behavior:

export const Route = createFileRoute('/users/$id')({
  loader: async ({ params }) => {
    const user = await fetchUser(params.id);
    return { user: userSchema.parse(user) };
  },
  component: UserDetail,
  errorComponent: ({ error }) => (
    <div className="text-red-600 p-4">Error: {error.message}</div>
  ),
  pendingComponent: () => (
    <div className="flex items-center justify-center p-4">
      <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
    </div>
  ),
});

UI Components

Always prefer Shadcn/ui components over custom ones:

import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';

<Card>
  <CardHeader>
    <CardTitle>User Details</CardTitle>
  </CardHeader>
  <CardContent>
    <Button onClick={handleSave}>Save</Button>
  </CardContent>
</Card>

Use Tailwind for styling with responsive design:

<div className="flex flex-col gap-4 p-6 md:flex-row md:gap-6">
  <Button className="w-full md:w-auto">Action</Button>
</div>

Accessibility

Use semantic HTML first. Only add ARIA when no semantic equivalent exists:

// ✅ Good: Semantic HTML with minimal ARIA
<button onClick={toggleMenu}>
  <MenuIcon aria-hidden="true" />
  <span className="sr-only">Toggle Menu</span>
</button>

// ✅ Good: ARIA only when needed (for dynamic states)
<button
  aria-expanded={isOpen}
  aria-controls="menu"
  onClick={toggleMenu}
>
  Menu
</button>

// ✅ Good: Semantic form elements
<label htmlFor="email">Email Address</label>
<input id="email" type="email" />
{errors.email && (
  <p role="alert">{errors.email}</p>
)}

File Organization

src/
├── components/ui/    # Shadcn/ui components
├── lib/schemas.ts    # Zod schemas
├── routes/          # File-based routes
└── routes/api/      # Server routes (.ts)

Import Standards

Use the project's configured path alias for internal imports; the examples below assume @/ is configured:

// ✅ Good
import { Button } from '@/components/ui/button'
import { userSchema } from '@/lib/schemas'

// ❌ Bad
import { Button } from '../components/ui/button'

Adding Components

Install Shadcn components when needed:

npx shadcn@latest add button card input dialog

Common Patterns

  • Always validate external data with Zod
  • Use route loaders for initial data, React Query for updates
  • Provide error/pending UI through router defaults or route overrides
  • Prefer Shadcn components over custom UI
  • Use the configured path alias consistently
  • Follow accessibility best practices