Web Styling (React)
Tailwind CSS
基本用法
function Button({ variant = 'primary', children }) {
const baseClasses = 'px-4 py-2 rounded-lg font-medium transition-colors';
const variants = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
danger: 'bg-red-600 text-white hover:bg-red-700',
};
return (
<button className={`${baseClasses} ${variants[variant]}`}>
{children}
</button>
);
}
条件类与 clsx/cn
import { clsx } from 'clsx';
// 或者使用 tailwind-merge 进行去重
import { twMerge } from 'tailwind-merge';
function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
function Card({ isActive, isDisabled, className, children }) {
return (
<div
className={cn(
'p-4 rounded-lg border',
isActive && 'border-blue-500 bg-blue-50',
isDisabled && 'opacity-50 cursor-not-allowed',
className // 允许覆盖
)}
>
{children}
</div>
);
}
响应式设计
// 移动优先断点
// sm: 640px, md: 768px, lg: 1024px, xl: 1280px, 2xl: 1536px
<div className="
grid
grid-cols-1 /* 移动: 1列 */
sm:grid-cols-2 /* 平板: 2列 */
lg:grid-cols-3 /* 桌面: 3列 */
xl:grid-cols-4 /* 大屏: 4列 */
gap-4
">
{items.map(item => <Card key={item.id} {...item} />)}
</div>
// 响应式文本
<h1 className="text-2xl md:text-3xl lg:text-4xl font-bold">
标题
</h1>
// 在断点处隐藏/显示
<nav className="hidden md:flex">桌面导航</nav>
<nav className="flex md:hidden">移动导航</nav>
暗色模式
// tailwind.config.js
module.exports = {
darkMode: 'class', // 或 'media' 以使用操作系统偏好
// ...
};
// 组件
<div className="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
<h1 className="text-black dark:text-white">标题</h1>
<p className="text-gray-600 dark:text-gray-400">描述</p>
</div>
// 切换暗色模式
function ThemeToggle() {
const [isDark, setIsDark] = useState(false);
useEffect(() => {
document.documentElement.classList.toggle('dark', isDark);
}, [isDark]);
return (
<button onClick={() => setIsDark(!isDark)}>
{isDark ? '☀️' : '🌙'}
</button>
);
}
自定义设计令牌
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
brand: {
50: '#f0f9ff',
100: '#e0f2fe',
500: '#0ea5e9',
600: '#0284c7',
700: '#0369a1',
},
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
spacing: {
'18': '4.5rem',
'88': '22rem',
},
},
},
};
// 使用
<button className="bg-brand-500 hover:bg-brand-600">
品牌按钮
</button>
CSS Modules
基本用法
/* Button.module.css */
.button {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-weight: 500;
}
.primary {
background-color: #3b82f6;
color: white;
}
.secondary {
background-color: #e5e7eb;
color: #1f2937;
}
// Button.tsx
import styles from './Button.module.css';
function Button({ variant = 'primary', children }) {
return (
<button className={`${styles.button} ${styles[variant]}`}>
{children}
</button>
);
}
与 clsx 一起使用
import styles from './Card.module.css';
import { clsx } from 'clsx';
function Card({ isActive, className, children }) {
return (
<div
className={clsx(
styles.card,
isActive && styles.active,
className
)}
>
{children}
</div>
);
}
CSS-in-JS (styled-components)
import styled from 'styled-components';
const Button = styled.button<{ variant?: 'primary' | 'secondary' }>`
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-weight: 500;
transition: background-color 0.2s;
${({ variant = 'primary' }) =>
variant === 'primary'
? `
background-color: #3b82f6;
color: white;
&:hover {
background-color: #2563eb;
}
`
: `
background-color: #e5e7eb;
color: #1f2937;
&:hover {
background-color: #d1d5db;
}
`}
`;
// 与主题一起使用
import { ThemeProvider } from 'styled-components';
const theme = {
colors: {
primary: '#3b82f6',
secondary: '#e5e7eb',
},
spacing: {
sm: '0.5rem',
md: '1rem',
},
};
const ThemedButton = styled.button`
background-color: ${({ theme }) => theme.colors.primary};
padding: ${({ theme }) => theme.spacing.md};
`;
组件变体模式
// 使用 cva (class-variance-authority) 与 Tailwind
import { cva, type VariantProps } from 'class-variance-authority';
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md font-medium transition-colors',
{
variants: {
variant: {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
outline: 'border border-gray-300 hover:bg-gray-50',
ghost: 'hover:bg-gray-100',
danger: 'bg-red-600 text-white hover:bg-red-700',
},
size: {
sm: 'h-8 px-3 text-sm',
md: 'h-10 px-4',
lg: 'h-12 px-6 text-lg',
},
},
defaultVariants: {
variant: 'primary',
size: 'md',
},
}
);
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
function Button({ variant, size, className, ...props }: ButtonProps) {
return (
<button
className={cn(buttonVariants({ variant, size }), className)}
{...props}
/>
);
}
// 使用
<Button variant="primary" size="lg">大号主要</Button>
<Button variant="outline">轮廓</Button>
动画模式
Tailwind 动画
// 内置动画
<div className="animate-spin">加载中...</div>
<div className="animate-pulse">加载中...</div>
<div className="animate-bounce">向下滚动</div>
// 过渡
<button className="transition-all duration-200 hover:scale-105">
悬停我
</button>
// 在 tailwind.config.js 中自定义动画
module.exports = {
theme: {
extend: {
animation: {
'fade-in': 'fadeIn 0.3s ease-out',
'slide-up': 'slideUp 0.3s ease-out',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
slideUp: {
'0%': { transform: 'translateY(10px)', opacity: '0' },
'100%': { transform: 'translateY(0)', opacity: '1' },
},
},
},
},
};
// 使用
<div className="animate-fade-in">渐入...</div>
Framer Motion
import { motion, AnimatePresence } from 'framer-motion';
function Modal({ isOpen, onClose, children }) {
return (
<AnimatePresence>
{isOpen && (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50"
onClick={onClose}
/>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
className="fixed inset-x-4 top-1/2 -translate-y-1/2 bg-white rounded-lg p-6"
>
{children}
</motion.div>
</>
)}
</AnimatePresence>
);
}
布局模式
Flexbox
// 居中
<div className="flex items-center justify-center min-h-screen">
<Card>居中内容</Card>
</div>
// 间隔
<div className="flex items-center justify-between">
<Logo />
<Navigation />
<UserMenu />
</div>
// 响应式方向
<div className="flex flex-col md:flex-row gap-4">
<Sidebar />
<Main />
</div>
Grid
// 等宽列
<div className="grid grid-cols-3 gap-4">
{items.map(item => <Card key={item.id} {...item} />)}
</div>
// 复杂布局
<div className="grid grid-cols-12 gap-4">
<aside className="col-span-3">侧边栏</aside>
<main className="col-span-6">主要内容</main>
<aside className="col-span-3">右侧边栏</aside>
</div>
// 自适应未知数量
<div className="grid grid-cols-[repeat(auto-fit,minmax(250px,1fr))] gap-4">
{items.map(item => <Card key={item.id} {...item} />)}
</div>
容器
// 居中容器,最大宽度
<div className="container mx-auto px-4">
<Content />
</div>
// 或自定义最大宽度
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<Content />
</div>
主题系统
// theme.ts
export const theme = {
colors: {
primary: {
50: '#eff6ff',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
},
gray: {
50: '#f9fafb',
100: '#f3f4f6',
900: '#111827',
},
},
spacing: {
xs: '0.25rem',
sm: '0.5rem',
md: '1rem',
lg: '1.5rem',
xl: '2rem',
},
} as const;
// CSS变量方法
:root {
--color-primary: #3b82f6;
--color-background: #ffffff;
--color-text: #111827;
}
.dark {
--color-primary: #60a5fa;
--color-background: #111827;
--color-text: #f9fafb;
}
// 使用Tailwind
<div className="bg-[var(--color-background)] text-[var(--color-text)]">
内容
</div>
常见问题
| 问题 |
解决方案 |
| 样式不应用 |
检查类特异性,Tailwind 清除 |
| 暗色模式闪烁 |
使用CSS变量或SSR安全方法 |
| 布局偏移 |
设置明确的尺寸,使用骨架加载器 |
| 移动端溢出 |
向body添加overflow-x-hidden |
| Z-index冲突 |
使用一致的z-index规模 |
文件结构
styles/
globals.css # 全局样式,Tailwind导入
variables.css # CSS自定义属性
components/
Button/
Button.tsx
Button.module.css # 如果使用CSS Modules
index.ts
tailwind.config.js # Tailwind配置