1.順風CSS
npm install -D tailwindcss @tailwindcss/vite
// Component với Tailwind
function Card({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div className="rounded-lg border bg-white p-6 shadow-sm dark:bg-gray-800">
<h2 className="mb-4 text-xl font-semibold text-gray-900 dark:text-white">
{title}
</h2>
<div className="text-gray-600 dark:text-gray-300">
{children}
</div>
</div>
);
}
// Conditional classes với clsx/cn
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
function Button({ variant, size, className, ...props }: ButtonProps) {
return (
<button
className={cn(
'rounded-md font-medium transition-colors',
{
'bg-blue-600 text-white hover:bg-blue-700': variant === 'primary',
'bg-gray-200 text-gray-800 hover:bg-gray-300': variant === 'secondary',
'bg-red-600 text-white hover:bg-red-700': variant === 'danger',
},
{
'px-3 py-1.5 text-sm': size === 'sm',
'px-4 py-2 text-base': size === 'md',
'px-6 py-3 text-lg': size === 'lg',
},
className
)}
{...props}
/>
);
}
2.shadcn/ui
# Initialize
npx shadcn@latest init
# Add components
npx shadcn@latest add button card input dialog
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import {
Dialog, DialogContent, DialogHeader,
DialogTitle, DialogTrigger,
} from '@/components/ui/dialog';
function UserCard({ user }: { user: User }) {
return (
<Card>
<CardHeader>
<CardTitle>{user.name}</CardTitle>
</CardHeader>
<CardContent>
<p>{user.email}</p>
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Edit</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit User</DialogTitle>
</DialogHeader>
<Input defaultValue={user.name} />
</DialogContent>
</Dialog>
</CardContent>
</Card>
);
}
3.成幀器運動
npm install motion
import { motion, AnimatePresence } from 'motion/react';
// Basic animation
function FadeIn({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
>
{children}
</motion.div>
);
}
// List animation
function AnimatedList({ items }: { items: Item[] }) {
return (
<AnimatePresence>
{items.map(item => (
<motion.div
key={item.id}
layout
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
>
{item.name}
</motion.div>
))}
</AnimatePresence>
);
}
// Page transition
function PageTransition({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, x: 100 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -100 }}
transition={{ type: 'tween', duration: 0.2 }}
>
{children}
</motion.div>
);
}
4. 深色模式
// useTheme hook
function useTheme() {
const [theme, setTheme] = useState<'light' | 'dark'>(() => {
if (typeof window === 'undefined') return 'light';
return (localStorage.getItem('theme') as 'light' | 'dark')
|| (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
});
useEffect(() => {
const root = document.documentElement;
root.classList.toggle('dark', theme === 'dark');
localStorage.setItem('theme', theme);
}, [theme]);
const toggleTheme = () => setTheme(prev => prev === 'light' ? 'dark' : 'light');
return { theme, toggleTheme };
}
// Toggle button
function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
return (
<button onClick={toggleTheme}>
{theme === 'light' ? '🌙' : '☀️'}
</button>
);
}
下一篇: Next.js 和應用程式路由器 — 基於檔案的路由、佈局、範本和專案結構。