A production-ready Next.js 15 starter. Radix UI, Tailwind v4, Jest, and Playwright — pre-wired so your team can focus on the product from day one.
pnpm install && pnpm devThree commands to go from zero to running.
git clone https://github.com/gfs-engineering/gfs-telephone.gitThen cd gfs-telephone && cp .env.example .env.local
pnpm installRequires pnpm ≥ 10. Run npm install -g pnpm@10 if not installed.
pnpm devNext.js 15 + Turbopack. Hot reload in ~2s → http://localhost:3007
Radix UI primitives styled with Tailwind v4. Import from @/components/ui.
import { Button } from '@/components/ui/button';
<Button>Default</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="link">Link</Button>import { Badge } from '@/components/ui/badge';
<Badge>Default</Badge>
<Badge variant="success">Active</Badge>
<Badge variant="warning">Pending</Badge>
<Badge variant="destructive">Failed</Badge>import { Alert, AlertTitle, AlertDescription } from '@/components/ui/alert';
<Alert variant="success">
<CheckCircle className="h-4 w-4" />
<AlertTitle>Payment processed</AlertTitle>
<AlertDescription>$45.00 collected successfully.</AlertDescription>
</Alert>Real Insurance
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
<Card>
<CardHeader>
<CardTitle>Life Cover</CardTitle>
<CardDescription>Real Insurance</CardDescription>
</CardHeader>
<CardContent>…</CardContent>
</Card>Client-side Radix primitives — click the <> Code button on each card to see the usage.
Alex Chen
Senior Developer
Sarah Liu
Product Manager
Everything comes from Tailwind v4 utility classes. No CSS files per component, no CSS-in-JS, no styled-components.
// Layout, spacing, typography — all as className
<div className="flex items-center gap-4 rounded-xl border bg-white p-6 shadow-sm">
<span className="text-sm font-medium text-gray-900">Label</span>
<span className="text-xs text-gray-500">Supporting text</span>
</div>
// Responsive — sm: md: lg: xl: 2xl:
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
// State variants — hover: focus: active: disabled: group-hover:
<button className="bg-brand-600 hover:bg-brand-700 disabled:opacity-50
focus-visible:ring-2 focus-visible:ring-brand-500">import { cn } from '@/lib/utils';
// Conditional classes — no string interpolation bugs
<div className={cn(
'rounded-lg border p-4', // always applied
isActive && 'border-brand-500 bg-brand-50',
isError && 'border-red-500 bg-red-50',
className, // forwarded className prop
)}>
// CVA for variant components (used in all UI components here)
import { cva } from 'class-variance-authority';
const variants = cva('base-classes', {
variants: {
size: { sm: 'h-8 px-3 text-xs', md: 'h-10 px-4 text-sm' },
},
});Defined in globals.css @theme — use as bg-brand-*, text-brand-*, border-brand-*, ring-brand-*
bg-brand-600bg-brand-700bg-brand-50ring-brand-500Never write CSS files
All component styling lives in className strings. If you reach for a .css file, check the Tailwind docs — there is almost certainly a utility for it.
No style="" for layout
Use Tailwind classes for all spacing, sizing, colours, and layout. Only use inline style for truly dynamic values (e.g. a progress bar width from JS).
Extend via @theme
Add new design tokens in globals.css inside the @theme block. A token like --color-surface becomes the class bg-surface in Tailwind v4.
Every decision is deliberate. No magic, no lock-in.
Conventions baked in so your team starts with good habits from day one.
./
├── src/
│ ├── app/
│ │ ├── _components/ # Client / reference components for this route
│ │ │ ├── demos.tsx # Interactive UI demos (use client)
│ │ │ └── intro.tsx # Reference page — delete import in page.tsx to start fresh
│ │ ├── globals.css # Tailwind v4 @theme tokens
│ │ ├── layout.tsx # Root layout — metadata, global styles
│ │ └── page.tsx # Your app starts here
│ ├── components/
│ │ └── ui/ # Radix + CVA component primitives
│ │ ├── index.ts # Barrel — import from '@/components/ui'
│ │ ├── alert.tsx
│ │ ├── avatar.tsx
│ │ ├── badge.tsx
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── checkbox.tsx
│ │ ├── dialog.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── separator.tsx
│ │ ├── switch.tsx
│ │ ├── tabs.tsx
│ │ ├── textarea.tsx
│ │ └── tooltip.tsx
│ ├── lib/
│ │ └── utils.ts # cn() — clsx + tailwind-merge
│ └── types/
│ └── global.d.ts # CSS module ambient declarations
├── e2e/ # Playwright E2E tests
├── .env.example # Required environment variables
├── CLAUDE.md # AI assistant guide for this project
├── jest.config.ts
├── next.config.ts
├── playwright.config.ts
└── tsconfig.jsonsrc/app/Next.js App Router. Each route is a folder. Server Components by default. Add "use client" only for browser APIs or state.
src/app/_components/Client components scoped to a route. The underscore prefix keeps them out of the URL routing. One file = one concern.
src/components/ui/Unstyled Radix primitives + Tailwind v4 + CVA. No business logic. Import everything from the barrel: import { Button, Badge } from "@/components/ui".
src/lib/utils.tscn() merges Tailwind classes with clsx + tailwind-merge. Use it everywhere to safely compose class strings without conflicts.
e2e/Playwright specs. webServer block auto-starts the app. Specs should test real user flows, not implementation details.
Official documentation for every tool in this starter.
App Router, Server Components, data fetching, routing
Hooks, context, Server vs Client Components
Types, generics, strict mode configuration
CSS-first config, @theme tokens, utility classes
Unstyled, accessible components — accordion to tooltip
Type-safe component variants with CVA
Fast package manager — workspaces, filtering, scripts
Unit and component testing, mocks, coverage
Render, query, and interact with components
End-to-end browser testing, trace viewer, codegen
Flat config, React + TypeScript lint rules
Browse and search the full icon set