| name | nextjs-tailwind-guidelines |
| description | Next.js 15 + Tailwind CSS development guidelines. Modern patterns including App Router, Server/Client Components, Suspense, lazy loading, file organization, Tailwind CSS styling, performance optimization, and TypeScript best practices. Use when creating components, pages, features, styling, routing, or working with Next.js code. |
Next.js + Tailwind Development Guidelines
Purpose
Comprehensive guide for modern Next.js 15 development with App Router, emphasizing Server Components, Suspense-based patterns, Tailwind CSS styling, and performance optimization.
When to Use This Skill
- Creating new components or pages
- Building new features with App Router
- Server vs Client Component decisions
- Styling with Tailwind CSS
- Next.js routing and layouts
- Performance optimization
- Organizing frontend code
- TypeScript best practices
Quick Start
New Component Checklist
- Server Component (default) or Client Component ('use client')?
- Use TypeScript with proper prop types
- Lazy load heavy components:
lazy(() => import()) - Wrap in
<Suspense>for loading states - Import alias:
@/for src directory - Tailwind classes with
className -
useCallbackfor event handlers passed to children - Default export at bottom
- No early returns with loading spinners
New Feature Checklist
- Create
src/features/{feature}/directory - Subdirectories:
components/,hooks/,lib/,types/ - TypeScript types in
types/ - Route in
src/app/{feature}/page.tsx - Lazy load client components when needed
- Suspense boundaries for async operations
- Export public API from
index.ts
Server vs Client Components
SERVER COMPONENTS (Default):
- Data fetching
- Static content
- SEO-critical content
- No interactivity needed
- Zero JavaScript to client
CLIENT COMPONENTS ('use client'):
- useState, useEffect, useContext
- Event handlers (onClick, onChange)
- Browser APIs (localStorage, window)
- Third-party libraries requiring browser
Key Rule: Default to Server Components, add 'use client' only when needed
Component Patterns
Server Component
// Server Component - No 'use client' needed
import { Suspense } from 'react';
interface Props {
id: string;
}
async function MyServerComponent({ id }: Props) {
// Fetch data directly in Server Component
const data = await fetch(`https://api.example.com/data/${id}`).then(r => r.json());
return (
<div className="p-4 bg-white dark:bg-gray-900">
<h1 className="text-2xl font-bold">{data.title}</h1>
</div>
);
}
export default MyServerComponent;
Client Component
'use client';
import { useState, useCallback } from 'react';
import { motion } from 'framer-motion';
interface Props {
onAction?: () => void;
}
export function MyClientComponent({ onAction }: Props) {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount(c => c + 1);
onAction?.();
}, [onAction]);
return (
<motion.button
onClick={handleClick}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
whileHover={{ scale: 1.05 }}
>
Clicked {count} times
</motion.button>
);
}
export default MyClientComponent;
Tailwind CSS Styling
Basic Patterns
// Utility classes
<div className="flex items-center justify-between p-4 bg-white dark:bg-gray-900">
// Responsive
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
// Conditional classes (simple)
<div className={`base-class ${isActive ? 'active' : 'inactive'}`}>
// Conditional classes (complex) - use cn utility
import { cn } from '@/lib/utils';
<div className={cn(
"base-class",
isActive && "active-class",
isLarge && "text-xl"
)}>
Common Patterns
// Card
<div className="rounded-lg border bg-card text-card-foreground shadow-sm">
// Button
<button className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90">
// Input
<input className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm">
App Router Structure
src/
app/
layout.tsx # Root layout
page.tsx # Home page (/)
about/
page.tsx # /about
blog/
layout.tsx # Blog layout
page.tsx # /blog
[slug]/
page.tsx # /blog/[slug]
api/
hello/
route.ts # API endpoint
Page Example
// src/app/my-route/page.tsx
import { Suspense } from 'react';
import { MyFeature } from '@/features/my-feature';
// Metadata for SEO
export const metadata = {
title: 'My Page',
description: 'Page description',
};
export default function MyPage() {
return (
<div className="container mx-auto py-8">
<Suspense fallback={<div className="animate-pulse">Loading...</div>}>
<MyFeature />
</Suspense>
</div>
);
}
Layout Example
// src/app/layout.tsx
import type { Metadata } from 'next';
import './globals.css';
export const metadata: Metadata = {
title: 'My App',
description: 'App description',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className="min-h-screen bg-background font-sans antialiased">
{children}
</body>
</html>
);
}
Data Fetching
Server Component (Recommended)
// Fetch directly in Server Component
async function MyComponent() {
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 } // Cache for 1 hour
}).then(r => r.json());
return <div>{data.title}</div>;
}
Client Component (when needed)
'use client';
import { useEffect, useState } from 'react';
export function MyClientData() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/data')
.then(r => r.json())
.then(setData)
.finally(() => setLoading(false));
}, []);
if (loading) return <div>Loading...</div>;
return <div>{data?.title}</div>;
}
Performance Optimization
Next.js Built-in
// Image optimization
import Image from 'next/image';
<Image
src="/hero.jpg"
alt="Hero"
width={800}
height={600}
priority // For above-fold images
/>
// Font optimization
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
// Dynamic imports (lazy loading)
import dynamic from 'next/dynamic';
const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
loading: () => <div>Loading...</div>,
ssr: false // Disable SSR if needed
});
React Optimization
'use client';
import { useMemo, useCallback, memo } from 'react';
// useMemo for expensive computations
const filtered = useMemo(() =>
items.filter(item => item.active).sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
// useCallback for event handlers
const handleClick = useCallback((id: string) => {
console.log(id);
}, []);
// memo for expensive components
const ExpensiveComponent = memo(({ data }: Props) => {
return <div>{/* Heavy rendering */}</div>;
});
Loading & Error States
Loading UI
// src/app/my-route/loading.tsx
export default function Loading() {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary" />
</div>
);
}
Error Handling
// src/app/my-route/error.tsx
'use client';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<h2 className="text-2xl font-bold mb-4">Something went wrong!</h2>
<button
onClick={reset}
className="px-4 py-2 bg-primary text-primary-foreground rounded"
>
Try again
</button>
</div>
);
}
File Organization
src/
app/ # Next.js App Router
layout.tsx
page.tsx
globals.css
components/ # Reusable UI components
ui/
button.tsx
card.tsx
layout/
header.tsx
footer.tsx
features/ # Feature-specific code
auth/
components/
hooks/
lib/
types/
index.ts
lib/ # Shared utilities
utils.ts
types/ # Global types
index.ts
TypeScript Best Practices
// Explicit prop types
interface ComponentProps {
title: string;
count?: number;
onAction: (id: string) => void;
}
// Type imports
import type { ReactNode } from 'react';
// Async Server Component typing
async function MyComponent(): Promise<JSX.Element> {
const data = await fetchData();
return <div>{data}</div>;
}
// Client Component typing
'use client';
export function MyClient({ children }: { children: ReactNode }) {
return <div>{children}</div>;
}
Animation with Framer Motion
'use client';
import { motion } from 'framer-motion';
export function AnimatedCard() {
return (
<motion.div
className="p-6 bg-card rounded-lg"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
whileHover={{ scale: 1.02 }}
>
Content
</motion.div>
);
}
Icons with Lucide React
import { Search, Menu, X, ChevronRight } from 'lucide-react';
export function IconExample() {
return (
<button className="flex items-center gap-2">
<Search className="h-4 w-4" />
<span>Search</span>
</button>
);
}
Core Principles
- Server Components First - Default to Server, add 'use client' only when needed
- Suspense for Loading - Use Suspense boundaries, not early returns
- Tailwind for Styling - Utility-first CSS with design tokens
- Features Organized - components/, hooks/, lib/, types/ subdirs
- Next.js Image & Font - Always use optimized components
- Import Aliases - Use @/ for clean imports
- No Early Returns - Prevents layout shift
- TypeScript Strict - Type everything properly
Skill Status: Adapted for Next.js 15 + Tailwind CSS v4