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:

  1. CSS Modules/Tailwind: Best performance (static extraction)

  2. Styled-Components: Server-side rendering support, but runtime cost

  3. Global CSS: Keep minimal, use CSS variables for theming

  4. 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!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?