DEV Community

Cover image for 10 Next.js Packages Every Developer Must Know Before Writing a Single Line of Code
sizan mahmud0
sizan mahmud0

Posted on

10 Next.js Packages Every Developer Must Know Before Writing a Single Line of Code

Starting a Next.js project without these packages is like building a house without power tools. Sure, you can do it, but why would you when better solutions exist?

After building 50+ Next.js applications, I've learned that the right packages at the start save weeks of work later. These aren't just nice-to-havesβ€”they're the difference between a prototype and a production-ready application.

If you're about to run npx create-next-app, stop. Read this first. These 10 packages will save you from countless headaches and make you look like a Next.js wizard.

1. Zod: Type-Safe Data Validation That Actually Works

Install: npm install zod

The Problem: User input is evil. Forms, API responses, environment variablesβ€”nothing can be trusted. TypeScript types disappear at runtime, leaving you vulnerable.

The Solution: Zod provides runtime validation with TypeScript type inference. It's like having a bouncer at every data entry point.

Basic Usage

import { z } from 'zod';

// Define a schema
const UserSchema = z.object({
  name: z.string().min(1, "Name is required"),
  email: z.string().email("Invalid email format"),
  age: z.number().min(18, "Must be 18 or older"),
  role: z.enum(['admin', 'user', 'guest']),
  website: z.string().url().optional(),
});

// Type inference (no need to write types twice!)
type User = z.infer<typeof UserSchema>;

// Validate data
try {
  const user = UserSchema.parse(formData);
  // user is now typed and validated
} catch (error) {
  if (error instanceof z.ZodError) {
    console.log(error.errors); // Detailed error messages
  }
}
Enter fullscreen mode Exit fullscreen mode

Real-World Next.js API Route

// app/api/users/route.ts
import { z } from 'zod';
import { NextRequest, NextResponse } from 'next/server';

const CreateUserSchema = z.object({
  name: z.string().min(2).max(50),
  email: z.string().email(),
  password: z.string().min(8).regex(
    /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
    "Password must contain uppercase, lowercase, and number"
  ),
  confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
  message: "Passwords don't match",
  path: ["confirmPassword"],
});

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const validatedData = CreateUserSchema.parse(body);

    // Now safely use validatedData
    const user = await createUser(validatedData);
    return NextResponse.json(user, { status: 201 });

  } catch (error) {
    if (error instanceof z.ZodError) {
      return NextResponse.json(
        { errors: error.errors },
        { status: 400 }
      );
    }
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Environment Variables Validation

// lib/env.ts
import { z } from 'zod';

const envSchema = z.object({
  DATABASE_URL: z.string().url(),
  NEXTAUTH_SECRET: z.string().min(32),
  STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
  NODE_ENV: z.enum(['development', 'production', 'test']),
});

// Validate on startup
export const env = envSchema.parse(process.env);

// Now you have type-safe, validated env vars
// env.DATABASE_URL is guaranteed to be a valid URL
Enter fullscreen mode Exit fullscreen mode

Why it's essential: Catches bugs at runtime, provides amazing error messages, and eliminates the need to write validation logic manually. Used by Vercel, Clerk, and thousands of Next.js apps.

2. React Hook Form + Zod: Forms Without Tears

Install: npm install react-hook-form @hookform/resolvers

The Problem: Forms in React are notoriously painful. Managing state, validation, errors, and submissions manually is error-prone and verbose.

The Solution: React Hook Form provides performant, flexible forms with minimal re-renders. Combine it with Zod for bulletproof validation.

'use client';

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const signupSchema = z.object({
  username: z.string().min(3, "Username must be at least 3 characters"),
  email: z.string().email("Invalid email address"),
  password: z.string().min(8, "Password must be at least 8 characters"),
  confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
  message: "Passwords don't match",
  path: ["confirmPassword"],
});

type SignupFormData = z.infer<typeof signupSchema>;

export default function SignupForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<SignupFormData>({
    resolver: zodResolver(signupSchema),
  });

  const onSubmit = async (data: SignupFormData) => {
    try {
      const response = await fetch('/api/auth/signup', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data),
      });

      if (response.ok) {
        // Handle success
        console.log('User created!');
      }
    } catch (error) {
      console.error('Signup failed:', error);
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
      <div>
        <input
          {...register('username')}
          placeholder="Username"
          className="w-full px-4 py-2 border rounded"
        />
        {errors.username && (
          <p className="text-red-500 text-sm mt-1">{errors.username.message}</p>
        )}
      </div>

      <div>
        <input
          {...register('email')}
          type="email"
          placeholder="Email"
          className="w-full px-4 py-2 border rounded"
        />
        {errors.email && (
          <p className="text-red-500 text-sm mt-1">{errors.email.message}</p>
        )}
      </div>

      <div>
        <input
          {...register('password')}
          type="password"
          placeholder="Password"
          className="w-full px-4 py-2 border rounded"
        />
        {errors.password && (
          <p className="text-red-500 text-sm mt-1">{errors.password.message}</p>
        )}
      </div>

      <div>
        <input
          {...register('confirmPassword')}
          type="password"
          placeholder="Confirm Password"
          className="w-full px-4 py-2 border rounded"
        />
        {errors.confirmPassword && (
          <p className="text-red-500 text-sm mt-1">{errors.confirmPassword.message}</p>
        )}
      </div>

      <button
        type="submit"
        disabled={isSubmitting}
        className="w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700 disabled:opacity-50"
      >
        {isSubmitting ? 'Creating account...' : 'Sign Up'}
      </button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Why it's essential:

  • Minimal re-renders (better performance)
  • Built-in validation
  • Easy error handling
  • Works perfectly with Server Actions
  • Reduces form code by 60%

3. ShadCN UI: Copy-Paste Components That Look Professional

Install: npx shadcn-ui@latest init

The Problem: Building UI from scratch is time-consuming. Component libraries like Material-UI or Chakra are great but add bloat and lock you into their ecosystem.

The Solution: ShadCN UI is not a libraryβ€”it's a collection of copy-paste components built with Radix UI and Tailwind. You own the code completely.

# Install components as needed
npx shadcn-ui@latest add button
npx shadcn-ui@latest add dialog
npx shadcn-ui@latest add dropdown-menu
Enter fullscreen mode Exit fullscreen mode

Example: Professional Modal Dialog

import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";

export function DeleteConfirmDialog({ onConfirm }: { onConfirm: () => void }) {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button variant="destructive">Delete Account</Button>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Are you absolutely sure?</DialogTitle>
          <DialogDescription>
            This action cannot be undone. This will permanently delete your
            account and remove your data from our servers.
          </DialogDescription>
        </DialogHeader>
        <div className="flex justify-end gap-3">
          <Button variant="outline">Cancel</Button>
          <Button variant="destructive" onClick={onConfirm}>
            Delete Account
          </Button>
        </div>
      </DialogContent>
    </Dialog>
  );
}
Enter fullscreen mode Exit fullscreen mode

Available Components:

  • Buttons, Forms, Inputs
  • Dialogs, Dropdowns, Tooltips
  • Tables, Cards, Tabs
  • Date Pickers, Calendars
  • And 40+ more

Why it's essential:

  • No runtime overhead (just your code)
  • Fully customizable with Tailwind
  • Accessible by default (Radix UI)
  • Production-ready styling
  • Copy once, modify forever

4. NextAuth.js: Authentication That Just Works

Install: npm install next-auth

The Problem: Building authentication from scratch is a security nightmare. Sessions, tokens, OAuth, password hashingβ€”it's complex and risky.

The Solution: NextAuth.js handles everything: email/password, OAuth (Google, GitHub, etc.), magic links, JWTs, and more.

// app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth";
import GoogleProvider from "next-auth/providers/google";
import GitHubProvider from "next-auth/providers/github";
import CredentialsProvider from "next-auth/providers/credentials";
import { PrismaAdapter } from "@auth/prisma-adapter";
import { prisma } from "@/lib/prisma";
import bcrypt from "bcryptjs";

const handler = NextAuth({
  adapter: PrismaAdapter(prisma),
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
    GitHubProvider({
      clientId: process.env.GITHUB_ID!,
      clientSecret: process.env.GITHUB_SECRET!,
    }),
    CredentialsProvider({
      name: "Credentials",
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" }
      },
      async authorize(credentials) {
        if (!credentials?.email || !credentials?.password) {
          return null;
        }

        const user = await prisma.user.findUnique({
          where: { email: credentials.email }
        });

        if (!user || !user.hashedPassword) {
          return null;
        }

        const isValid = await bcrypt.compare(
          credentials.password,
          user.hashedPassword
        );

        if (!isValid) {
          return null;
        }

        return {
          id: user.id,
          email: user.email,
          name: user.name,
        };
      }
    }),
  ],
  session: {
    strategy: "jwt",
  },
  pages: {
    signIn: '/login',
    signOut: '/logout',
    error: '/error',
  },
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.id = user.id;
      }
      return token;
    },
    async session({ session, token }) {
      if (session.user) {
        session.user.id = token.id as string;
      }
      return session;
    },
  },
});

export { handler as GET, handler as POST };
Enter fullscreen mode Exit fullscreen mode

Using Auth in Components

'use client';

import { useSession, signIn, signOut } from "next-auth/react";

export default function ProfileButton() {
  const { data: session, status } = useSession();

  if (status === "loading") {
    return <div>Loading...</div>;
  }

  if (session) {
    return (
      <div className="flex items-center gap-3">
        <span>Welcome, {session.user?.name}</span>
        <button onClick={() => signOut()}>Sign Out</button>
      </div>
    );
  }

  return <button onClick={() => signIn()}>Sign In</button>;
}
Enter fullscreen mode Exit fullscreen mode

Protecting Server Components

import { getServerSession } from "next-auth/next";
import { redirect } from "next/navigation";

export default async function ProtectedPage() {
  const session = await getServerSession();

  if (!session) {
    redirect('/login');
  }

  return <h1>Welcome, {session.user?.name}!</h1>;
}
Enter fullscreen mode Exit fullscreen mode

Why it's essential: Authentication is hard. NextAuth.js is battle-tested, secure, and handles edge cases you didn't know existed.

5. Prisma: The ORM That Makes Databases Fun

Install: npm install prisma @prisma/client && npx prisma init

The Problem: Writing raw SQL is error-prone. Traditional ORMs are clunky and slow. Type safety between database and code is manual.

The Solution: Prisma provides type-safe database access with an intuitive API, migrations, and works perfectly with Next.js Server Components.

// prisma/schema.prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String?
  posts     Post[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Post {
  id        String   @id @default(cuid())
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}
Enter fullscreen mode Exit fullscreen mode

Type-Safe Database Queries

// lib/prisma.ts
import { PrismaClient } from '@prisma/client';

const globalForPrisma = global as unknown as { prisma: PrismaClient };

export const prisma = globalForPrisma.prisma || new PrismaClient();

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;

// app/api/posts/route.ts
import { prisma } from '@/lib/prisma';
import { NextResponse } from 'next/server';

export async function GET() {
  const posts = await prisma.post.findMany({
    where: { published: true },
    include: {
      author: {
        select: {
          name: true,
          email: true,
        },
      },
    },
    orderBy: {
      createdAt: 'desc',
    },
    take: 10,
  });

  return NextResponse.json(posts);
}

export async function POST(request: Request) {
  const body = await request.json();

  const post = await prisma.post.create({
    data: {
      title: body.title,
      content: body.content,
      author: {
        connect: { id: body.authorId },
      },
    },
  });

  return NextResponse.json(post, { status: 201 });
}
Enter fullscreen mode Exit fullscreen mode

Why it's essential:

  • Auto-generated TypeScript types
  • Intuitive query API
  • Built-in migrations
  • Excellent Next.js integration
  • Active development and community

6. TanStack Query (React Query): Server State Management Done Right

Install: npm install @tanstack/react-query

The Problem: Managing server state (API data) with useState/useEffect is a mess. Caching, refetching, loading statesβ€”it's boilerplate hell.

The Solution: TanStack Query handles all server state concerns: caching, background updates, pagination, infinite scroll, and more.

// app/providers.tsx
'use client';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { useState } from 'react';

export function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient());

  return (
    <QueryClientProvider client={queryClient}>
      {children}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Using in Components

'use client';

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

interface Post {
  id: string;
  title: string;
  content: string;
}

export function PostList() {
  const queryClient = useQueryClient();

  // Fetch posts with automatic caching
  const { data: posts, isLoading, error } = useQuery({
    queryKey: ['posts'],
    queryFn: async () => {
      const response = await fetch('/api/posts');
      if (!response.ok) throw new Error('Failed to fetch posts');
      return response.json() as Promise<Post[]>;
    },
    staleTime: 5 * 60 * 1000, // 5 minutes
    refetchOnWindowFocus: true,
  });

  // Mutation for creating posts
  const createPost = useMutation({
    mutationFn: async (newPost: Omit<Post, 'id'>) => {
      const response = await fetch('/api/posts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(newPost),
      });
      return response.json();
    },
    onSuccess: () => {
      // Invalidate and refetch posts
      queryClient.invalidateQueries({ queryKey: ['posts'] });
    },
  });

  if (isLoading) return <div>Loading posts...</div>;
  if (error) return <div>Error loading posts</div>;

  return (
    <div>
      {posts?.map((post) => (
        <div key={post.id}>
          <h3>{post.title}</h3>
          <p>{post.content}</p>
        </div>
      ))}
      <button onClick={() => createPost.mutate({ title: 'New Post', content: 'Content' })}>
        Create Post
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Why it's essential:

  • Eliminates 90% of data-fetching boilerplate
  • Automatic caching and background updates
  • Built-in loading/error states
  • Optimistic updates
  • Works perfectly with Next.js Server Components

7. Zustand: State Management That Doesn't Hurt

Install: npm install zustand

The Problem: Redux is overkill. Context API causes unnecessary re-renders. You need simple global state without the ceremony.

The Solution: Zustand provides a minimal, fast state management solution with zero boilerplate.

// store/useAuthStore.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface User {
  id: string;
  name: string;
  email: string;
}

interface AuthStore {
  user: User | null;
  token: string | null;
  login: (user: User, token: string) => void;
  logout: () => void;
  updateUser: (user: Partial<User>) => void;
}

export const useAuthStore = create<AuthStore>()(
  persist(
    (set) => ({
      user: null,
      token: null,
      login: (user, token) => set({ user, token }),
      logout: () => set({ user: null, token: null }),
      updateUser: (updates) =>
        set((state) => ({
          user: state.user ? { ...state.user, ...updates } : null,
        })),
    }),
    {
      name: 'auth-storage', // Persists to localStorage
    }
  )
);

// store/useCartStore.ts
interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
}

interface CartStore {
  items: CartItem[];
  addItem: (item: CartItem) => void;
  removeItem: (id: string) => void;
  updateQuantity: (id: string, quantity: number) => void;
  clearCart: () => void;
  total: () => number;
}

export const useCartStore = create<CartStore>((set, get) => ({
  items: [],
  addItem: (item) =>
    set((state) => {
      const exists = state.items.find((i) => i.id === item.id);
      if (exists) {
        return {
          items: state.items.map((i) =>
            i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
          ),
        };
      }
      return { items: [...state.items, item] };
    }),
  removeItem: (id) =>
    set((state) => ({ items: state.items.filter((i) => i.id !== id) })),
  updateQuantity: (id, quantity) =>
    set((state) => ({
      items: state.items.map((i) => (i.id === id ? { ...i, quantity } : i)),
    })),
  clearCart: () => set({ items: [] }),
  total: () => {
    const state = get();
    return state.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  },
}));
Enter fullscreen mode Exit fullscreen mode

Using in Components

'use client';

import { useAuthStore } from '@/store/useAuthStore';
import { useCartStore } from '@/store/useCartStore';

export function Header() {
  const user = useAuthStore((state) => state.user);
  const logout = useAuthStore((state) => state.logout);
  const cartItems = useCartStore((state) => state.items);
  const cartTotal = useCartStore((state) => state.total());

  return (
    <header>
      {user ? (
        <div>
          <span>Welcome, {user.name}</span>
          <button onClick={logout}>Logout</button>
        </div>
      ) : (
        <a href="/login">Login</a>
      )}
      <div>
        Cart: {cartItems.length} items (${cartTotal.toFixed(2)})
      </div>
    </header>
  );
}
Enter fullscreen mode Exit fullscreen mode

Why it's essential:

  • Minimal API (learn in 5 minutes)
  • No providers needed
  • Automatic optimization (no unnecessary re-renders)
  • Middleware for persistence, devtools, etc.
  • Perfect for small to medium state needs

8. Axios or Ky: Better HTTP Requests

Install: npm install axios or npm install ky

The Problem: The native fetch API is powerful but verbose. Error handling, interceptors, and timeouts require boilerplate.

The Solution: Axios (traditional) or Ky (modern, lightweight) provide cleaner APIs with built-in features.

Axios Example

// lib/api.ts
import axios from 'axios';

const api = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL,
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
  },
});

// Request interceptor (add auth token)
api.interceptors.request.use((config) => {
  const token = localStorage.getItem('token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Response interceptor (handle errors globally)
api.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      // Redirect to login
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

export default api;

// Usage
import api from '@/lib/api';

export async function fetchUsers() {
  const response = await api.get('/users');
  return response.data;
}

export async function createUser(userData: any) {
  const response = await api.post('/users', userData);
  return response.data;
}
Enter fullscreen mode Exit fullscreen mode

Ky Example (Modern Alternative)

// lib/api.ts
import ky from 'ky';

const api = ky.create({
  prefixUrl: process.env.NEXT_PUBLIC_API_URL,
  timeout: 10000,
  hooks: {
    beforeRequest: [
      (request) => {
        const token = localStorage.getItem('token');
        if (token) {
          request.headers.set('Authorization', `Bearer ${token}`);
        }
      },
    ],
    afterResponse: [
      async (request, options, response) => {
        if (response.status === 401) {
          window.location.href = '/login';
        }
      },
    ],
  },
});

export default api;

// Usage
const users = await api.get('users').json();
const newUser = await api.post('users', { json: userData }).json();
Enter fullscreen mode Exit fullscreen mode

Why it's essential:

  • Cleaner syntax than fetch
  • Built-in interceptors
  • Automatic JSON parsing
  • Timeout handling
  • TypeScript support

9. Date-fns: Date Manipulation Without the Pain

Install: npm install date-fns

The Problem: JavaScript Date objects are awful. Moment.js is deprecated and massive (67KB). You need modern date utilities.

The Solution: date-fns is modular, tree-shakeable, and provides 200+ functions for date manipulation.

import { 
  format, 
  formatDistance, 
  formatRelative,
  addDays,
  subDays,
  isAfter,
  isBefore,
  parseISO,
  startOfDay,
  endOfDay,
  differenceInDays
} from 'date-fns';

// Formatting
const date = new Date();
format(date, 'yyyy-MM-dd'); // "2025-12-24"
format(date, 'MMMM dd, yyyy'); // "December 24, 2025"
format(date, "h:mm a"); // "3:45 PM"

// Relative time
formatDistance(subDays(date, 3), date, { addSuffix: true });
// "3 days ago"

formatRelative(subDays(date, 3), date);
// "last Friday at 3:45 PM"

// Date math
const tomorrow = addDays(date, 1);
const lastWeek = subDays(date, 7);

// Comparisons
isAfter(date, lastWeek); // true
isBefore(date, tomorrow); // true

// Parsing
const parsed = parseISO('2025-12-24T15:30:00');

// Day boundaries
const dayStart = startOfDay(date); // 2025-12-24 00:00:00
const dayEnd = endOfDay(date);     // 2025-12-24 23:59:59

// Differences
differenceInDays(date, subDays(date, 10)); // 10
Enter fullscreen mode Exit fullscreen mode

Real-World Next.js Usage

// components/PostCard.tsx
import { formatDistance } from 'date-fns';

interface Post {
  id: string;
  title: string;
  createdAt: Date;
}

export function PostCard({ post }: { post: Post }) {
  return (
    <article>
      <h3>{post.title}</h3>
      <time>
        Posted {formatDistance(post.createdAt, new Date(), { addSuffix: true })}
      </time>
    </article>
  );
}

// lib/utils.ts
import { format, isToday, isYesterday, isThisYear } from 'date-fns';

export function smartDateFormat(date: Date): string {
  if (isToday(date)) {
    return `Today at ${format(date, 'h:mm a')}`;
  }
  if (isYesterday(date)) {
    return `Yesterday at ${format(date, 'h:mm a')}`;
  }
  if (isThisYear(date)) {
    return format(date, 'MMM d');
  }
  return format(date, 'MMM d, yyyy');
}
Enter fullscreen mode Exit fullscreen mode

Why it's essential:

  • Tree-shakeable (only import what you use)
  • Immutable (no date mutation bugs)
  • TypeScript-first
  • No timezone headaches
  • 2KB vs Moment.js 67KB

10. Framer Motion: Animations That Wow

Install: npm install framer-motion

The Problem: CSS animations are limited. Complex gestures, layout animations, and scroll-triggered effects require JavaScript.

The Solution: Framer Motion makes advanced animations simple with a declarative API built for React.


typescript
'use client';

import { motion, AnimatePresence } from 'framer-motion';
import { useState } from 'react';

// Fade in animation
export function FadeInText() {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.5 }}
    >
      <h1>Welcome to our site!</h1>
    </motion.div>
  );
}

// Hover and tap animations
export function AnimatedButton() {
  return (
    <motion.button
      whileHover={{ scale: 1.05 }}
      whileTap={{ scale: 0.95 }}
      className="px-6 py-3 bg-blue-600 text-white rounded"
    >
      Click me
    </motion.button>
  );
}

// List animations
export function TodoList({ todos }: { todos: string[] }) {
  return (
    <ul>
      <AnimatePresence>
        {todos.map((todo, index) => (
          <motion.li
            key={todo}
            initial={{ opacity: 0, x: -50 }}
            animate={{ opacity: 1, x: 0 }}
            exit={{ opacity: 0, x: 50 }}
            transition={{ delay: index * 0.1 }}
          >
            {todo}
          </motion.li>
        ))}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)