React & Next.js Topic
Next.js Styling & UI Architecture
Overview of Styling Approaches
Modern React applications use various styling methodologies, each with different trade-offs for scalability, performance, and developer experience.
1. CSS Modules
CSS Modules are CSS files where all class names are scoped locally by default, preventing naming conflicts.
Basic Usage
css
/* components/Button/Button.module.css */
.button {
padding: 12px 24px;
border: none;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
transition: all 0.2s ease;
}
.primary {
background-color: #3b82f6;
color: white;
}
.primary:hover {
background-color: #2563eb;
}
.secondary {
background-color: #6b7280;
color: white;
}
.secondary:hover {
background-color: #4b5563;
}
.loading {
opacity: 0.7;
cursor: not-allowed;
}
jsx
// components/Button/Button.jsx
import styles from './Button.module.css';
function Button({
children,
variant = 'primary',
loading = false,
...props
}) {
const className = [
styles.button,
styles[variant],
loading && styles.loading
].filter(Boolean).join(' ');
return (
<button className={className} disabled={loading} {...props}>
{loading ? 'Loading...' : children}
</button>
);
}
export default Button;
Composition with CSS Modules
css
/* components/Card/Card.module.css */
.card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 20px;
margin: 16px 0;
}
.header {
border-bottom: 1px solid #e5e7eb;
padding-bottom: 12px;
margin-bottom: 16px;
}
.title {
font-size: 1.25rem;
font-weight: 600;
color: #111827;
margin: 0;
}
/* Composition example */
.compact {
composes: card;
padding: 12px;
margin: 8px 0;
}
jsx
export default Card;
2. Tailwind CSS
Tailwind CSS is a utility-first CSS framework that lets you build designs directly in your markup.
Setup & Configuration
js
// tailwind.config.js
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
'./app/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {
colors: {
brand: {
50: '#f0f9ff',
500: '#3b82f6',
600: '#2563eb',
900: '#1e3a8a',
}
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
},
},
plugins: [],
};
Basic Usage
jsx
// components/Button/Button.jsx
function Button({
children,
variant = 'primary',
size = 'md',
loading = false,
...props
}) {
const baseClasses = 'font-medium rounded-lg transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed';
const variants = {
primary: 'bg-brand-500 hover:bg-brand-600 text-white focus:ring-brand-500',
secondary: 'bg-gray-500 hover:bg-gray-600 text-white focus:ring-gray-500',
outline: 'border border-gray-300 hover:bg-gray-50 text-gray-700 focus:ring-brand-500',
};
const sizes = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
};
const className = [
baseClasses,
variants[variant],
sizes[size],
loading && 'opacity-70'
].filter(Boolean).join(' ');
return (
<button className={className} disabled={loading} {...props}>
{loading ? (
<span className="flex items-center">
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-current" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Loading...
</span>
) : (
children
)}
</button>
);
}
export default Button;
Component Composition with Tailwind
jsx
// components/Card/Card.jsx
function Card({
title,
children,
actions,
variant = 'default',
className = ''
}) {
const variants = {
default: 'bg-white shadow-sm border border-gray-200',
elevated: 'bg-white shadow-lg border border-gray-100',
filled: 'bg-gray-50 border border-gray-200',
};
return (
<div className={`rounded-lg ${variants[variant]} ${className}`}>
{title && (
<div className="px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-semibold text-gray-900">{title}</h3>
</div>
)}
<div className="px-6 py-4">
{children}
</div>
{actions && (
<div className="px-6 py-4 border-t border-gray-200 bg-gray-50 rounded-b-lg">
{actions}
</div>
)}
</div>
);
}
// Usage
function UserProfile() {
return (
<Card
title="User Profile"
variant="elevated"
className="max-w-md mx-auto"
actions={
<div className="flex space-x-3">
<Button variant="primary" size="sm">Save</Button>
<Button variant="outline" size="sm">Cancel</Button>
</div>
}
>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700">Name</label>
<input
type="text"
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-500 focus:ring-brand-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Email</label>
<input
type="email"
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-500 focus:ring-brand-500"
/>
</div>
</div>
</Card>
);
}
Tailwind with CSS-in-JS Pattern
jsx
// hooks/useTailwindClass.js
import { useMemo } from 'react';
export function useTailwindClass(classes) {
return useMemo(() => {
return Array.isArray(classes)
? classes.filter(Boolean).join(' ')
: classes;
}, [classes]);
}
// components/Alert/Alert.jsx
import { useTailwindClass } from '../../hooks/useTailwindClass';
function Alert({
children,
variant = 'info',
dismissible = false,
onDismiss
}) {
const baseClasses = useTailwindClass([
'p-4 rounded-lg border',
dismissible && 'pr-12'
]);
const variantClasses = {
info: 'bg-blue-50 border-blue-200 text-blue-800',
success: 'bg-green-50 border-green-200 text-green-800',
warning: 'bg-yellow-50 border-yellow-200 text-yellow-800',
error: 'bg-red-50 border-red-200 text-red-800',
};
const className = useTailwindClass([baseClasses, variantClasses[variant]]);
return (
<div className={className} role="alert">
{children}
{dismissible && (
<button
onClick={onDismiss}
className="absolute top-4 right-4 p-1 rounded hover:bg-black hover:bg-opacity-10 transition-colors"
>
<span className="sr-only">Dismiss</span>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
)}
</div>
);
}
3. Styled-Components
Styled-Components is a CSS-in-JS library that lets you write actual CSS in your JavaScript.
Basic Usage
jsx
// components/Button/Button.styles.js
import styled, { css, keyframes } from 'styled-components';
const spin = keyframes`
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
`;
const ButtonBase = styled.button`
padding: 12px 24px;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
outline: none;
&:focus {
box-shadow: 0 0 0 2px ${props => props.theme.colors.primary}40;
}
&:disabled {
opacity: 0.7;
cursor: not-allowed;
}
${props => props.$loading && css`
opacity: 0.7;
cursor: wait;
`}
`;
const PrimaryButton = styled(ButtonBase)`
background-color: ${props => props.theme.colors.primary};
color: white;
&:hover:not(:disabled) {
background-color: ${props => props.theme.colors.primaryDark};
}
`;
const SecondaryButton = styled(ButtonBase)`
background-color: ${props => props.theme.colors.gray};
color: white;
&:hover:not(:disabled) {
background-color: ${props => props.theme.colors.grayDark};
}
`;
const LoadingSpinner = styled.div`
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
animation: ${spin} 1s linear infinite;
margin-right: 8px;
`;
export { PrimaryButton, SecondaryButton, LoadingSpinner };
jsx
// components/Button/Button.jsx
import { PrimaryButton, SecondaryButton, LoadingSpinner } from './Button.styles';
function Button({
children,
variant = 'primary',
loading = false,
...props
}) {
const StyledButton = variant === 'primary' ? PrimaryButton : SecondaryButton;
return (
<StyledButton $loading={loading} {...props}>
{loading && <LoadingSpinner />}
{children}
</StyledButton>
);
}
export default Button;
Theming with Styled-Components
jsx
// styles/theme.js
export const theme = {
colors: {
primary: '#3b82f6',
primaryDark: '#2563eb',
gray: '#6b7280',
grayDark: '#4b5563',
white: '#ffffff',
black: '#000000',
},
spacing: {
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
xl: '32px',
},
breakpoints: {
mobile: '768px',
tablet: '1024px',
desktop: '1200px',
},
shadows: {
sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
md: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
},
};
// styles/GlobalStyles.js
import { createGlobalStyle } from 'styled-components';
export const GlobalStyles = createGlobalStyle`
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
line-height: 1.5;
color: ${props => props.theme.colors.black};
background-color: #f8fafc;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
@media (max-width: ${props => props.theme.breakpoints.mobile}) {
html {
font-size: 14px;
}
}
`;
jsx
// pages/_app.js (Next.js)
import { ThemeProvider } from 'styled-components';
import { GlobalStyles } from '../styles/GlobalStyles';
import { theme } from '../styles/theme';
function MyApp({ Component, pageProps }) {
return (
<ThemeProvider theme={theme}>
<GlobalStyles />
<Component {...pageProps} />
</ThemeProvider>
);
}
export default MyApp;
Advanced Styled-Components Patterns
jsx
// components/Card/Card.styles.js
import styled, { css } from 'styled-components';
export const Card = styled.div`
background: ${props => props.theme.colors.white};
border-radius: 8px;
box-shadow: ${props => props.theme.shadows.md};
padding: ${props => props.theme.spacing.lg};
margin: ${props => props.theme.spacing.md} 0;
border: 1px solid #e5e7eb;
${props => props.$variant === 'elevated' && css`
box-shadow: ${props => props.theme.shadows.lg};
border-color: #f3f4f6;
`}
${props => props.$variant === 'filled' && css`
background: #f9fafb;
border-color: #e5e7eb;
box-shadow: none;
`}
${props => props.$compact && css`
padding: ${props => props.theme.spacing.md};
margin: ${props => props.theme.spacing.sm} 0;
`}
`;
export const CardHeader = styled.div`
border-bottom: 1px solid #e5e7eb;
padding-bottom: ${props => props.theme.spacing.md};
margin-bottom: ${props => props.theme.spacing.md};
`;
export const CardTitle = styled.h3`
font-size: 1.25rem;
font-weight: 600;
color: #111827;
margin: 0;
`;
export const CardContent = styled.div`
color: #6b7280;
`;
export const CardActions = styled.div`
display: flex;
gap: ${props => props.theme.spacing.sm};
margin-top: ${props => props.theme.spacing.md};
padding-top: ${props => props.theme.spacing.md};
border-top: 1px solid #f3f4f6;
`;
jsx
// components/Card/Card.jsx
import {
Card as StyledCard,
CardHeader,
CardTitle,
CardContent,
CardActions
} from './Card.styles';
function Card({
title,
children,
actions,
variant = 'default',
compact = false,
...props
}) {
return (
<StyledCard $variant={variant} $compact={compact} {...props}>
{title && (
<CardHeader>
<CardTitle>{title}</CardTitle>
</CardHeader>
)}
<CardContent>
{children}
</CardContent>
{actions && (
<CardActions>
{actions}
</CardActions>
)}
</StyledCard>
);
}
export default Card;
4. Global vs Component-Level Styles
Global Styles
css
/* styles/globals.css */
:root {
--color-primary: #3b82f6;
--color-primary-dark: #2563eb;
--color-gray-100: #f3f4f6;
--color-gray-800: #1f2937;
--spacing-xs: 4px;
--spacing-md: 16px;
--border-radius: 6px;
}
/* Reset and base styles */
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
line-height: 1.5;
color: var(--color-gray-800);
background-color: white;
}
/* Utility classes */
.text-center { text-align: center; }
.text-left { text-align: left; }
.text-right { text-align: right; }
.flex { display: flex; }
.flex-col { flex-direction: column; }
.items-center { align-items: center; }
.justify-between { justify-content: space-between; }
.m-0 { margin: 0; }
.p-0 { padding: 0; }
/* Dark mode support */
@media (prefers-color-scheme: dark) {
:root {
--color-gray-800: #f9fafb;
--color-gray-100: #1f2937;
}
}
Component-Level Styles
jsx
// components/Layout/Layout.module.css
.layout {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.header {
background: white;
border-bottom: 1px solid #e5e7eb;
padding: 1rem 0;
position: sticky;
top: 0;
z-index: 50;
}
.main {
flex: 1;
padding: 2rem 0;
}
.footer {
background: #f9fafb;
border-top: 1px solid #e5e7eb;
padding: 2rem 0;
margin-top: auto;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
}
@media (min-width: 768px) {
.container {
padding: 0 2rem;
}
}
Hybrid Approach
jsx
// components/ThemeProvider/ThemeProvider.jsx
import { createContext, useContext } from 'react';
const ThemeContext = createContext();
export function ThemeProvider({ children, theme = {} }) {
const contextValue = {
colors: {
primary: '#3b82f6',
secondary: '#6b7280',
...theme.colors,
},
spacing: {
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
...theme.spacing,
},
...theme,
};
return (
<ThemeContext.Provider value={contextValue}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
// Custom hook for responsive styles
export function useResponsive() {
return {
isMobile: '(max-width: 768px)',
isTablet: '(max-width: 1024px)',
isDesktop: '(min-width: 1025px)',
};
}
Comparison & Best Practices
| Method | Scoping | Performance | Bundle Size | Learning Curve |
|---|---|---|---|---|
| CSS Modules | Local by default | Excellent | Small | Easy |
| Tailwind CSS | Utility classes | Excellent | Medium (can optimize) | Medium |
| Styled-Components | Component-scoped | Good (runtime) | Larger | Medium |
When to Use Each:
CSS Modules:
-
Team prefers traditional CSS
-
Need predictable, static styling
-
Working with legacy codebases
Tailwind CSS:
-
Rapid prototyping and development
-
Consistent design system
-
Utility-first mindset fits team workflow
Styled-Components:
-
Dynamic, theme-based styling
-
Complex component variants
-
Strong TypeScript integration needed
Recommended Architecture:
jsx
// Example: Hybrid approach in a Next.js app
import '../styles/globals.css'; // Global styles & CSS variables
import { ThemeProvider } from '../components/ThemeProvider';
function MyApp({ Component, pageProps }) {
return (
<ThemeProvider>
{/* Use Tailwind for layout & utilities */}
<div className="min-h-screen bg-gray-50">
{/* Use CSS Modules for complex components */}
<Header className={styles.header} />
{/* Use styled-components for theme-aware components */}
<MainContent>
<Component {...pageProps} />
</MainContent>
</div>
</ThemeProvider>
);
}
Performance Considerations:
-
CSS Modules/Tailwind: Best performance (static extraction)
-
Styled-Components: Server-side rendering support, but runtime cost
-
Global CSS: Keep minimal, use CSS variables for theming
-
Critical CSS: Extract for above-the-fold content
Choose based on your team's preferences, project requirements, and performance needs. Many teams successfully combine these approaches for different parts of their application.
Comments (0)
No comments yet. Be the first to comment!