Tagline: From API to UI — where pixels meet products.
A modern Next.js + TypeScript starter template designed for learning and building web applications with best practices built in.
This template is designed for:
- 11-12 year olds who have completed CS50 or similar introductory programming courses
- Junior developers learning modern web development
- QA engineers wanting to understand frontend architecture
- Designers transitioning to development
Purpose: Provide a clean, well-tested foundation with strong guardrails that teaches good coding habits while staying simple enough to build upon.
# Install dependencies (requires Node.js 18+)
pnpm install
# Start development server
pnpm dev
# Open http://localhost:3000 in your browserevolve-ui/
├── src/
│ ├── app/ # Next.js App Router pages
│ │ ├── layout.tsx # Root layout with global styles
│ │ ├── page.tsx # Homepage
│ │ ├── globals.css # Tailwind CSS imports
│ │ └── api/ # API routes
│ │ ├── products/ # Products endpoint
│ │ └── health/ # Health check endpoint
│ ├── components/ # Reusable React components
│ │ ├── Button.tsx # Example button component
│ │ └── HealthCheck.tsx # Client-side fetch demo
│ └── lib/ # Utility functions
│ └── formatMoney.ts # Currency formatting helper
├── __tests__/ # Jest unit tests
│ ├── lib/
│ │ └── formatMoney.test.ts
│ └── components/
│ └── Button.test.tsx
├── e2e/ # Playwright end-to-end tests
│ └── home.spec.ts
├── eslint.config.mjs # ESLint configuration
├── jest.config.ts # Jest configuration
├── playwright.config.ts # Playwright configuration
├── tsconfig.json # TypeScript configuration
└── package.json # Dependencies and scripts
- Strict mode enabled: Catches more bugs during development
- Path aliases: Use
@/instead of../../for imports - ES2022 target: Modern JavaScript features
Enforces code quality with:
- No unused imports: Auto-removes with
pnpm lint:fix - TypeScript best practices: Prevents common mistakes
- Explicit return types: Warns when functions don't have return types
- No
anytypes: Warns when usinganyinstead of proper types - Prefer const: Encourages immutable variables
- Uses Next.js built-in configuration
- Supports TypeScript and path aliases
- Includes
@testing-library/jest-dommatchers
- Runs tests in Chromium
- Auto-starts dev server for testing
- Generates HTML reports
This project uses shadcn/ui - a collection of reusable components built with Radix UI and Tailwind CSS.
What is shadcn/ui?
- Not a traditional component library (doesn't install via npm)
- Components are copied directly into your project
- Full control over the code
- Built with Radix UI for accessibility
- Styled with Tailwind CSS
- Supports dark mode out of the box
Included Components:
Button- Multiple variants (default, outline, ghost, destructive)Card- Container with header, content, footer sectionsBadge- Labels and status indicatorsSwitch- Toggle switch for settings
Adding More Components:
npx shadcn@latest add [component-name]Examples:
npx shadcn@latest add input # Add Input component
npx shadcn@latest add dialog # Add Dialog component
npx shadcn@latest add dropdown-menu # Add Dropdown MenuUsing Components:
import { Button } from "@/components/ui/button";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
<Card>
<CardHeader>
<CardTitle>Hello</CardTitle>
</CardHeader>
<CardContent>
<Button>Click me</Button>
</CardContent>
</Card>Customization:
All components use CSS variables defined in globals.css. You can customize the entire theme by changing these variables:
:root {
--primary: 221.2 83.2% 53.3%; /* Blue for light mode */
--background: 0 0% 100%; /* White background */
/* ... more variables */
}
.dark {
--primary: 217.2 91.2% 59.8%; /* Lighter blue for dark mode */
--background: 222.2 84% 4.9%; /* Dark background */
}This template uses Tailwind CSS v4, which represents a major shift in how Tailwind works.
Philosophy Shift:
- From "utility-first" to "utility-native": Tailwind v4 is now built from the ground up for modern CSS
- Faster builds: New engine built in Rust makes everything much faster
- Simpler config: Less configuration needed, more works out of the box
- Native CSS features: Better use of modern CSS capabilities like CSS variables and
@layer
Key Changes:
- Zero-config by default: Works without
tailwind.config.jsfor basic usage - Better performance: Up to 10x faster builds in some cases
- CSS-first approach: Generates cleaner, more maintainable CSS
- Modern color spaces: Built-in support for OKLCH colors
What This Means for You:
- Faster development: Changes show up quicker in your browser
- Less setup: Fewer configuration files to worry about
- Better colors: More vibrant, consistent colors across devices
- Future-proof: Uses the latest CSS standards
What is OKLCH?
OKLCH is a modern color space that describes colors using three values:
- L (Lightness): How bright the color is (0% = black, 100% = white)
- C (Chroma): How colorful it is (0 = gray, higher = more vibrant)
- H (Hue): The color itself (0-360 degrees around the color wheel)
Example:
/* OKLCH format */
--primary: 62.8% 0.214 259.6;
/* ^^^^ ^^^^^ ^^^^^
L C H
62.8% 0.214 259.6° = Beautiful blue */Why OKLCH instead of HSL or RGB?
The older formats (HSL, RGB, Hex) have problems:
-
HSL Problem: Colors with the same "lightness" value look different to our eyes
/* Both at 50% lightness, but yellow looks much brighter than blue */ --yellow-hsl: 60 100% 50%; /* Looks very bright */ --blue-hsl: 240 100% 50%; /* Looks much darker */
-
OKLCH Solution: Perceptually uniform - 50% lightness looks the same across all colors
/* Both at 62.8% lightness, and they actually look similar brightness */ --yellow-oklch: 62.8% 0.214 100; /* Consistent brightness */ --blue-oklch: 62.8% 0.214 259.6; /* Consistent brightness */
Benefits of OKLCH:
- Perceptually Uniform: Colors with the same lightness value actually look the same brightness to human eyes
- Wider Color Gamut: Access to more vibrant colors that HSL/RGB can't represent
- Better Gradients: Smooth transitions between colors without muddy middle tones
- Accessibility: Easier to create accessible color contrasts
- Future-Proof: Modern browsers natively support it
Real-World Example:
In this project, our primary blue is defined as:
:root {
--primary: 62.8% 0.214 259.6;
/* Bright Vibrant Blue */
}
.dark {
--primary: 70.9% 0.221 263.4;
/* Lighter More vibrant Slightly different blue */
}This gives us:
- Light mode: A vibrant blue that's readable
- Dark mode: A slightly brighter, more vibrant blue that stands out against dark backgrounds
- Consistent perception: Both feel equally "bright" in their contexts
CSS Variables Approach:
This template uses CSS variables to define all colors in one place (src/app/globals.css):
@layer base {
:root {
/* Light mode colors */
--background: 100% 0 0; /* Pure white */
--foreground: 9.8% 0.038 265.75; /* Almost black */
--primary: 62.8% 0.214 259.6; /* Blue */
}
.dark {
/* Dark mode colors */
--background: 13.4% 0.034 265.75; /* Very dark blue */
--foreground: 98% 0.013 264.5; /* Almost white */
--primary: 70.9% 0.221 263.4; /* Brighter blue */
}
}How It Works:
- Define once: Set all color values in
globals.css - Reference everywhere: Use these values throughout your app
<div className="bg-primary text-primary-foreground"> This uses the CSS variables automatically </div>
- Automatic dark mode: When
.darkclass is added to<html>, all colors switch
Tailwind Config Connection:
The tailwind.config.ts tells Tailwind to use these CSS variables:
colors: {
primary: {
DEFAULT: "oklch(var(--primary))",
foreground: "oklch(var(--primary-foreground))",
},
// ... more colors
}Benefits:
- Single source of truth: Change colors in one place
- Type-safe: Tailwind autocomplete works
- Runtime switching: Can change themes without rebuilding
- Consistent: Same colors across all components
Customizing Your Theme:
To change colors, edit src/app/globals.css:
:root {
/* Change primary to green */
--primary: 65% 0.18 145; /* L: 65%, C: 0.18, H: 145° (green) */
}Pro Tip: Use tools like OKLCH Color Picker to find colors you like, then copy the values directly into your CSS.
The template includes dark mode support using next-themes:
- System Detection: Automatically uses system preference
- Manual Toggle: Click the sun/moon icon in the header
- Persistent: Choice is saved in localStorage
- No Flash: Uses
suppressHydrationWarningto prevent flash on load
The ThemeProvider in layout.tsx wraps the entire app and makes theme context available everywhere.
This template demonstrates several Next.js App Router patterns:
src/app/
├── page.tsx → /
├── about/
│ └── page.tsx → /about
Blog Example (/blog/*):
src/app/blog/
├── layout.tsx ← Shared layout for all blog pages
├── page.tsx → /blog (blog index)
└── [slug]/
└── page.tsx → /blog/any-post (dynamic route)
The blog/layout.tsx provides a sidebar that persists across all blog pages. This is perfect for:
- Navigation menus
- Filters/search
- Category lists
- Anything that should stay visible while content changes
Dashboard Example (/dashboard/*):
src/app/dashboard/
├── layout.tsx ← Shared sidebar for all dashboard pages
├── page.tsx → /dashboard (overview)
├── analytics/
│ └── page.tsx → /dashboard/analytics
└── settings/
└── page.tsx → /dashboard/settings
The dashboard demonstrates a common admin pattern with:
- Persistent sidebar navigation
- Quick stats section
- Multiple sub-routes that share the layout
Dynamic routes use [paramName] syntax:
// src/app/blog/[slug]/page.tsx
interface BlogPostPageProps {
params: Promise<{ slug: string }>;
}
export default async function BlogPostPage(props: BlogPostPageProps) {
const params = await props.params;
const post = await getPost(params.slug);
return <div>{post.title}</div>;
}Server Components (default):
- No
"use client"directive - Can fetch data directly
- No hooks or browser APIs
- Better performance
// src/app/page.tsx - Server Component
export default async function Home() {
const data = await fetch(...); // Direct data fetching
return <div>...</div>;
}Client Components:
- Has
"use client"at the top - Can use hooks (useState, useEffect, etc.)
- Can access browser APIs
- Interactive features
// src/components/theme-toggle.tsx - Client Component
"use client";
import { useTheme } from "next-themes";
export function ThemeToggle() {
const { theme, setTheme } = useTheme(); // Hook usage
return <button onClick={() => setTheme(...)}>...</button>;
}All scripts use pnpm - install it globally with npm install -g pnpm if needed.
# Start development server on http://localhost:3000
pnpm dev
# Build for production
pnpm build
# Start production server
pnpm start# Run ESLint to check for issues
pnpm lint
# Auto-fix ESLint issues
pnpm lint:fix
# Type-check TypeScript without building
pnpm typecheck# Run all Jest tests
pnpm test
# Run tests in watch mode (re-runs on file changes)
pnpm test:watch
# Run Playwright e2e tests
pnpm e2e
# Run e2e tests with interactive UI
pnpm e2e:ui
# View last e2e test report
pnpm e2e:reportBefore committing code:
pnpm lint:fix # Fix linting issues
pnpm typecheck # Check for TypeScript errors
pnpm test # Run unit tests
pnpm build # Ensure production build works- Next.js 16 with App Router
- React 19 with TypeScript
- Tailwind CSS for styling
- shadcn/ui for beautiful, accessible components
- next-themes for dark mode support
- Lucide Icons for iconography
- ESLint with strict TypeScript checking
- TypeScript in strict mode
- Prettier-friendly config
- Jest for unit/integration tests
- React Testing Library for component tests
- Playwright for end-to-end tests
- API Routes:
/api/productsand/api/health - Server Components: Homepage with data fetching
- Client Components:
HealthCheck,ThemeTogglewith loading/error states - Utility Functions:
formatMoney,cnwith tests - shadcn/ui Components: Button, Card, Badge, Switch
- Nested Layouts: Blog and Dashboard with sidebar navigation
- Dynamic Routes:
/blog/[slug]for individual posts
- Start with:
src/app/page.tsx- See how pages work - Then explore:
src/components/Button.tsx- Reusable components - Try testing:
__tests__/components/Button.test.tsx- Learn testing - Build an API:
src/app/api/health/route.ts- Create endpoints - Add utilities:
src/lib/formatMoney.ts- Helper functions
- Server vs Client Components: See the difference in
page.tsxvsHealthCheck.tsx - TypeScript Types: Every function has clear types
- API Routes: RESTful endpoints with
/api/* - Error Handling: Loading and error states in
HealthCheck.tsx - Testing: Unit tests for utilities, component tests, and e2e tests
Solution:
npm install -g pnpmSolution:
# Kill the process using port 3000
# Windows:
netstat -ano | findstr :3000
taskkill /PID <PID> /F
# Mac/Linux:
lsof -ti:3000 | xargs kill -9
# Or use a different port:
pnpm dev -- -p 3001Solution:
pnpm lint:fixSolution:
# Restart TypeScript server in VS Code
Ctrl+Shift+P → "TypeScript: Restart TS Server"
# Or reload window
Ctrl+Shift+P → "Developer: Reload Window"Solution:
# Clear Jest cache
pnpm test --clearCache
# Reinstall dependencies
rm -rf node_modules
pnpm installSolution:
pnpx playwright installSolution:
Check that imports use @/ for src files:
// ✅ Correct
import { Button } from "@/components/Button";
// ❌ Wrong
import { Button } from "../components/Button";Solution:
Ensure globals.css is imported in layout.tsx:
import "./globals.css";Solution:
Check jest.config.ts has the correct moduleNameMapper:
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/src/$1",
}Solution:
# Run the same checks manually
pnpm lint:fix
pnpm typecheck
pnpm testSolution:
Make sure you've added the component:
npx shadcn@latest add button
npx shadcn@latest add cardCheck the import path uses @/components/ui/...:
import { Button } from "@/components/ui/button"; // ✅ Correct
import { Button } from "../components/ui/button"; // ❌ WrongSolution:
Ensure suppressHydrationWarning is on the <html> tag:
<html lang="en" suppressHydrationWarning>Check that ThemeProvider wraps your app in layout.tsx.
Solution:
Make sure globals.css has the CSS variables and Tailwind directives:
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
/* ... more variables */
}
}Solution:
Create the utils file:
// src/lib/utils.ts
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}Solution:
Check that your layout.tsx is in the correct directory and exports a default function:
// src/app/blog/layout.tsx
export default function BlogLayout({ children }: { children: ReactNode }) {
return <div>{/* sidebar */}{children}</div>;
}- Add a database: Try Prisma with SQLite
- Add authentication: Use NextAuth.js
- Add more pages: Create
/about,/contact - Style components: Build a component library
- Add forms: Use React Hook Form with validation
- Deploy: Try Vercel or Netlify
- Week 1: Understand the existing code
- Week 2: Add a new page and component
- Week 3: Create your own API endpoint
- Week 4: Write tests for your code
- Week 5: Build a small project (todo list, blog, etc.)
This is a learning template - feel free to modify anything! If you find issues or have suggestions, create an issue or pull request.
MIT - Feel free to use this for learning and building your own projects.