Styling trong React
CSS Modules
CSS Modules tạo scoped CSS, tránh conflict tên class.
// Button.module.css
.button {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.primary {
background-color: #007bff;
color: white;
}
.secondary {
background-color: #6c757d;
color: white;
}
.button:hover {
opacity: 0.9;
}
// Button.tsx
import styles from './Button.module.css';
import clsx from 'clsx'; // hoặc classnames
interface ButtonProps {
variant?: 'primary' | 'secondary';
children: React.ReactNode;
onClick?: () => void;
}
function Button({ variant = 'primary', children, onClick }: ButtonProps) {
return (
<button
className={clsx(styles.button, styles[variant])}
onClick={onClick}
>
{children}
</button>
);
}
// Class tên được hash: button_primary__xHk3a (tự động unique)
Tailwind CSS
Utility-first CSS framework, được dùng phổ biến nhất hiện nay.
npm install tailwindcss @tailwindcss/vite
// Component với Tailwind
function ProductCard({ product }: { product: Product }) {
return (
<div className="rounded-lg border border-gray-200 p-4 shadow-sm hover:shadow-md transition-shadow">
<img
src={product.imageUrl}
alt={product.name}
className="h-48 w-full object-cover rounded-md"
/>
<div className="mt-3">
<h3 className="text-lg font-semibold text-gray-900">{product.name}</h3>
<p className="mt-1 text-sm text-gray-500">{product.description}</p>
<div className="mt-3 flex items-center justify-between">
<span className="text-xl font-bold text-blue-600">
${product.price}
</span>
<button className="rounded-md bg-blue-600 px-4 py-2 text-sm text-white hover:bg-blue-700 disabled:opacity-50">
Add to Cart
</button>
</div>
</div>
</div>
);
}
clsx / tailwind-merge - Conditional Classes
npm install clsx tailwind-merge
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
// Utility function (nên tạo trong lib/utils.ts)
function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// Sử dụng
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
className?: string;
children: React.ReactNode;
}
function Button({
variant = 'primary',
size = 'md',
disabled,
className,
children,
}: ButtonProps) {
return (
<button
disabled={disabled}
className={cn(
// Base styles
'inline-flex items-center justify-center rounded-md font-medium transition-colors focus:outline-none focus:ring-2',
// Variant styles
{
'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500':
variant === 'primary',
'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500':
variant === 'secondary',
'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500':
variant === 'danger',
},
// Size styles
{
'h-8 px-3 text-sm': size === 'sm',
'h-10 px-4 text-base': size === 'md',
'h-12 px-6 text-lg': size === 'lg',
},
// Disabled
{ 'cursor-not-allowed opacity-50': disabled },
// Allow className override
className
)}
>
{children}
</button>
);
}
styled-components
CSS-in-JS, tạo styled components với template literals.
npm install styled-components
npm install -D @types/styled-components
import styled, { css, ThemeProvider, createGlobalStyle } from 'styled-components';
// Global styles
const GlobalStyle = createGlobalStyle`
*, *::before, *::after {
box-sizing: border-box;
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
}
`;
// Theme
const theme = {
colors: {
primary: '#007bff',
secondary: '#6c757d',
danger: '#dc3545',
background: '#f8f9fa',
},
spacing: (n: number) => `${n * 4}px`,
borderRadius: '6px',
};
// Styled components
const Card = styled.div`
background: white;
border-radius: ${({ theme }) => theme.borderRadius};
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: ${({ theme }) => theme.spacing(4)};
`;
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
}
const Button = styled.button<ButtonProps>`
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: opacity 0.2s;
&:hover {
opacity: 0.9;
}
&:disabled {
cursor: not-allowed;
opacity: 0.5;
}
${({ variant = 'primary', theme }) =>
variant === 'primary'
? css`
background: ${theme.colors.primary};
color: white;
`
: variant === 'danger'
? css`
background: ${theme.colors.danger};
color: white;
`
: css`
background: ${theme.colors.secondary};
color: white;
`}
${({ size = 'md' }) =>
size === 'sm'
? css`padding: 4px 12px; font-size: 14px;`
: size === 'lg'
? css`padding: 12px 24px; font-size: 18px;`
: css`padding: 8px 16px; font-size: 16px;`}
`;
// Extend styled component
const PrimaryButton = styled(Button).attrs({ variant: 'primary' })`
text-transform: uppercase;
letter-spacing: 0.5px;
`;
// App với ThemeProvider
function App() {
return (
<ThemeProvider theme={theme}>
<GlobalStyle />
<Card>
<h2>Product</h2>
<Button variant="primary">Add to Cart</Button>
<Button variant="danger" size="sm">Delete</Button>
</Card>
</ThemeProvider>
);
}
So sánh các Styling Approaches
| CSS Modules | Tailwind CSS | styled-components | |
|---|---|---|---|
| Bundle size | Nhỏ | Nhỏ (purge) | Lớn hơn |
| DX | Tốt | Rất tốt | Tốt |
| Type-safe | Hạn chế | ❌ | ✅ (với TS) |
| Theming | Thủ công | Config file | Theme object |
| Runtime | ❌ | ❌ | ✅ CSS-in-JS |
| Learning curve | Thấp | Trung bình | Trung bình |
| Phù hợp | Mọi dự án | Dự án mới | Component library |
Khuyến nghị
- Dự án mới: Tailwind CSS + clsx/tw-merge
- Component library: styled-components hoặc vanilla-extract
- Legacy codebase: CSS Modules