Production SaaS: dashboards, pricing pages, data tables, onboarding, role-based UI — with WCAG 2.1 AA accessibility and Core Web Vitals performance baked in.
<quick_start>
Setup: Tailwind v4 + shadcn/ui
npx create-next-app@latest my-app --typescript --tailwind --eslint --app --src-dir cd my-app && npx shadcn@latest init npx shadcn@latest add button card dialog table form
Tailwind v4 — CSS-First (No tailwind.config.js)
/* app/globals.css */ @import "tailwindcss"; @theme inline { --color-background: oklch(1 0 0); --color-foreground: oklch(0.145 0 0); --color-primary: oklch(0.205 0.042 264.695); --color-primary-foreground: oklch(0.985 0 0); --radius-lg: 0.5rem; --radius-md: calc(var(--radius-lg) - 2px); --radius-sm: calc(var(--radius-lg) - 4px); }
Component Anatomy (shadcn/ui 2026)
import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils"
const buttonVariants = cva( "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors", { variants: { variant: { default: "bg-primary text-primary-foreground", outline: "border border-input" }, size: { default: "h-10 px-4 py-2", sm: "h-9 px-3", lg: "h-11 px-8" }, }, defaultVariants: { variant: "default", size: "default" }, } )
// React 19: ref is a regular prop — no forwardRef // data-slot: styling hook for parent overrides function Button({ className, variant, size, ref, ...props }: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants>) { return <button ref={ref} data-slot="button" className={cn(buttonVariants({ variant, size, className }))} {...props} /> }
cn() Utility
import { clsx, type ClassValue } from "clsx" import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) }
Vite SPA Alternative
npm create vite@latest my-app -- --template react-ts cd my-app && npm i -D @tailwindcss/vite && npx shadcn@latest init
Key differences from Next.js:
-
@tailwindcss/vite plugin (not postcss) — faster HMR, native Vite integration
-
VITE_ env prefix (not NEXT_PUBLIC_ ), accessed via import.meta.env
-
Client-only — no Server Components, use React Query for data fetching
-
React.lazy()
- <Suspense> replaces dynamic() for code splitting
- Routing via React Router v7 or TanStack Router (not file-based)
Tailwind v4, shadcn/ui, component patterns, accessibility, forms, and performance guidance all apply equally to Vite SPAs. Only routing and data fetching genuinely differ.
See reference/vite-react-setup.md and reference/spa-routing.md . </quick_start>
<success_criteria> Enterprise SaaS frontend is production-ready when:
-
Accessible: WCAG 2.1 AA — keyboard nav, screen reader, focus management, 4.5:1 contrast
-
Performant: LCP < 2.5s, INP < 200ms, CLS < 0.1 on 4G mobile
-
Responsive: Mobile-first, works 320px-2560px, container queries for components
-
Secure: No XSS vectors, CSP headers, sanitized user content
-
Themed: Dark mode via CSS, design tokens in @theme, consistent spacing/color
-
Composable: Server Components default, client boundary pushed to leaves
-
Typed: TypeScript strict, Zod validation on all forms, no any
</success_criteria>
<core_principles>
-
Server-First — Default to Server Components. Add "use client" only for interactivity. Push client boundaries to leaf components.
-
Accessible-by-Default — Semantic HTML first (<nav> , <main> , <article> ). ARIA only when native semantics insufficient.
-
Composition Over Configuration — Small composable components. Compound pattern for complex UI. Context at boundaries.
-
Progressive Disclosure — Essential info first. Reveal complexity on demand. Reduce cognitive load.
-
Mobile-First — Design for smallest screen, enhance upward. Container queries for components. Touch targets >= 44px.
-
Design Tokens — All visual values in CSS @theme . Never hardcode. OKLCH for perceptual uniformity.
-
Type Safety E2E — Zod schemas shared client/server. React.ComponentProps<> over manual interfaces. </core_principles>
<tailwind_v4>
Tailwind CSS v4 — Key Changes from v3
-
No tailwind.config.js — All config via CSS @theme directive
-
@import "tailwindcss" — Replaces @tailwind base/components/utilities
-
OKLCH colors — Perceptually uniform, replaces hex/HSL
-
Container queries built-in — @container , @md: , @lg: prefixes
-
@source — CSS-native file scanning (replaces content array)
-
70% smaller CSS — Automatic unused style elimination
-
@theme inline — shadcn/ui bridge: tokens without generated utilities
@theme { --color-brand-500: oklch(0.55 0.15 250); --font-sans: "Inter", system-ui, sans-serif; --breakpoint-xs: 475px; --animate-slide-in: slide-in 0.2s ease-out; }
// Container queries — component-level responsive <div className="@container"> <div className="grid grid-cols-1 @md:grid-cols-2 @lg:grid-cols-3 gap-4"> {items.map(item => <Card key={item.id} {...item} />)} </div> </div>
Migration: npx @tailwindcss/upgrade — See reference/tailwind-v4-setup.md . </tailwind_v4>
<shadcn_ui>
shadcn/ui 2026
-
@theme inline — Bridges tokens with Tailwind v4
-
data-slot — Attribute-based styling hooks (replaces className overrides)
-
No forwardRef — React 19 ref as prop
-
tw-animate-css — Replaces tailwindcss-animate for v4 compat
-
Radix or Base UI — Choose primitive library
// data-slot: parent can target child styles function Card({ className, ref, ...props }: React.ComponentProps<"div">) { return <div ref={ref} data-slot="card" className={cn("rounded-xl border bg-card", className)} {...props} /> }
// Style from parent: <div className="[&_[data-slot=card]]:shadow-lg"> <Card>...</Card> </div>
Dark mode: CSS custom property swap with .dark class. See reference/shadcn-setup.md . </shadcn_ui>
<component_architecture>
Server vs Client Components
Server Component (default) Client Component ("use client" )
Async data fetching, DB access useState, useEffect, event handlers
Zero JS bundle, access to secrets Browser APIs, third-party client libs
Rule: Push "use client" to smallest leaf possible.
// Server page with client island export default async function DashboardPage() { const metrics = await getMetrics() return ( <main> <KPICards data={metrics} /> {/* Server-rendered /} <RevenueChart data={metrics} /> {/ Client island */} </main> ) }
Key Patterns
-
Compound components — <Table>/<TableRow>/<TableCell> namespace composition
-
cva variants — Type-safe style variants with class-variance-authority
-
React.ComponentProps — Replace manual interfaces, ref as regular prop
-
data-slot — External styling hooks for parent-child overrides
-
Polymorphic (asChild) — Slot pattern for rendering as different elements
-
SPA code splitting — React.lazy()
- <Suspense> replaces Next.js dynamic()
See reference/component-patterns.md for complete examples. </component_architecture>
<saas_patterns>
Enterprise SaaS Patterns
Dashboard: Sidebar + Header + Main
<div className="flex h-screen"> <Sidebar className="w-64 hidden lg:flex" /> <div className="flex-1 flex flex-col"> <Header /> {/* Search, user menu, notifications */} <main className="flex-1 overflow-auto p-6"> <KPIGrid metrics={metrics} /> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6"> <RevenueChart data={revenue} /> <ActivityFeed items={activities} /> </div> </main> </div> </div>
Pricing (3-Tier Conversion)
Anchor (low) | Conversion target (highlighted, "Most Popular") | Enterprise (custom)
Monthly/annual toggle, feature comparison table, social proof. See templates/pricing-page.tsx .
Data Tables — shadcn Table + TanStack Table for sort/filter/paginate
State Trio — Every data component needs: Loading (Skeleton) | Error (retry action) | Empty (guidance)
Role-Based UI — hasPermission(user, "scope") guard for conditional rendering
See reference/saas-dashboard.md and reference/saas-pricing-checkout.md . </saas_patterns>
Semantic HTML first — <header> , <nav> , <main> , <article> , <section> , <footer>
Pattern Implementation
Keyboard nav Tab/Shift+Tab, Arrow keys in menus/tabs, Escape to close
Focus management Trap in dialogs, restore on close, skip link
ARIA live regions aria-live="polite" for dynamic content
Form errors aria-invalid , aria-describedby , role="alert"
Loading states aria-busy={true} on loading buttons
Contrast 4.5:1 text, 3:1 UI components (OKLCH lightness channel)
// Skip link <a href="#main-content" className="sr-only focus:not-sr-only focus:absolute focus:z-50"> Skip to main content </a>
See reference/accessibility-checklist.md for per-component ARIA patterns.
<state_management>
State Decision Tree
State Type Solution Example
URL state nuqs / useSearchParams
Filters, pagination, tabs
Server data React Query / SWR API data, user profile
Local UI useState
Form inputs, toggles
Shared parent-child Lift state / Context Accordion groups
Complex cross-cutting Zustand Cart, wizard, notifications
Prefer URL state — shareable, bookmarkable, survives refresh. </state_management>
<data_fetching>
Data Fetching
Pattern When How
Server Components Default async function Page() { const data = await db.query() }
Suspense streaming Slow data <Suspense fallback={<Skeleton/>}><SlowComponent/></Suspense>
Server Actions Mutations "use server"
- revalidatePath()
React Query Client real-time useQuery({ queryKey, queryFn, refetchInterval })
React Query (SPA) Client-only apps useQuery({ queryKey, queryFn }) with loaders — replaces Server Components
</data_fetching>
-
Shared Zod schema — Single source of truth for client validation and server action
-
React Hook Form — useForm with zodResolver , mode: "onBlur"
-
shadcn Form — <Form>/<FormField>/<FormItem>/<FormLabel>/<FormMessage>
-
Server Action — safeParse on server, return field errors, revalidatePath
const schema = z.object({ name: z.string().min(2), email: z.string().email(), })
See reference/form-patterns.md and templates/form-with-server-action.tsx .
Metric Target Quick Win
LCP < 2.5s Main content visible next/image with priority , next/font
INP < 200ms Responsive interactions Code-split heavy components with dynamic()
CLS < 0.1 No layout shift Reserve space for images/fonts, Skeleton loaders
Tailwind v4 produces 70% smaller CSS automatically. See reference/performance-optimization.md .
Accessibility
-
Keyboard navigation for all interactive elements
-
Screen reader announces content meaningfully
-
Focus indicators visible, skip link present
-
Color contrast >= 4.5:1 (text), >= 3:1 (UI)
Performance
-
LCP < 2.5s, INP < 200ms, CLS < 0.1
-
Images via next/image, fonts via next/font
-
Heavy components code-split with dynamic()
Responsive
-
Works 320px-2560px, touch targets >= 44px
-
Container queries for reusable components
Security
-
No raw HTML injection without sanitization
-
CSP headers, Zod validation client AND server
UX
-
Loading / error / empty states for all data views
-
Toast for mutations, confirm for destructive actions