Skip to main content

Styling

CSS integration, Tailwind CSS, styled-components, CSS Modules, and styling approaches for Next.js applications.

CSS Integration

Global CSS

/* app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

* {
box-sizing: border-box;
padding: 0;
margin: 0;
}

html,
body {
max-width: 100vw;
overflow-x: hidden;
}

body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
);
}

/* Custom CSS variables */
:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}

@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
}
// app/layout.tsx
import './globals.css';

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}

Component-Level CSS

/* app/components/Button.module.css */
.button {
padding: 0.5rem 1rem;
border: none;
border-radius: 0.25rem;
cursor: pointer;
font-weight: 500;
transition: all 0.2s;
}

.primary {
background-color: #3b82f6;
color: white;
}

.primary:hover {
background-color: #2563eb;
}

.secondary {
background-color: #6b7280;
color: white;
}

.secondary:hover {
background-color: #4b5563;
}

.large {
padding: 0.75rem 1.5rem;
font-size: 1.125rem;
}

.small {
padding: 0.25rem 0.75rem;
font-size: 0.875rem;
}
// app/components/Button.tsx
import styles from './Button.module.css';

interface ButtonProps {
children: React.ReactNode;
variant?: 'primary' | 'secondary';
size?: 'small' | 'large';
onClick?: () => void;
}

export default function Button({
children,
variant = 'primary',
size,
onClick
}: ButtonProps) {
const classes = [
styles.button,
styles[variant],
size && styles[size]
].filter(Boolean).join(' ');

return (
<button className={classes} onClick={onClick}>
{children}
</button>
);
}

Tailwind CSS

Installation and Setup

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
colors: {
primary: {
50: '#eff6ff',
500: '#3b82f6',
900: '#1e3a8a',
},
secondary: {
50: '#f9fafb',
500: '#6b7280',
900: '#111827',
},
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
mono: ['Fira Code', 'monospace'],
},
spacing: {
72: '18rem',
84: '21rem',
96: '24rem',
},
},
},
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
require('@tailwindcss/aspect-ratio'),
],
};

Tailwind Components

// app/components/Card.tsx
interface CardProps {
title: string;
content: string;
image?: string;
className?: string;
}

export default function Card({ title, content, image, className = '' }: CardProps) {
return (
<div className={`bg-white rounded-lg shadow-md overflow-hidden ${className}`}>
{image && (
<img
src={image}
alt={title}
className="w-full h-48 object-cover"
/>
)}
<div className="p-6">
<h3 className="text-xl font-semibold text-gray-900 mb-2">{title}</h3>
<p className="text-gray-600 leading-relaxed">{content}</p>
</div>
</div>
);
}

Responsive Design

// app/components/Hero.tsx
export default function Hero() {
return (
<section className="bg-gradient-to-r from-blue-600 to-purple-600 text-white py-20">
<div className="container mx-auto px-4">
<div className="max-w-4xl mx-auto text-center">
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold mb-6">
Build Amazing Apps with Next.js
</h1>
<p className="text-xl md:text-2xl mb-8 opacity-90">
The React framework for production
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<button className="bg-white text-blue-600 px-8 py-3 rounded-lg font-semibold hover:bg-gray-100 transition-colors">
Get Started
</button>
<button className="border border-white text-white px-8 py-3 rounded-lg font-semibold hover:bg-white hover:text-blue-600 transition-colors">
Learn More
</button>
</div>
</div>
</div>
</section>
);
}

Dark Mode

// app/components/ThemeProvider.tsx
'use client';

import { createContext, useContext, useEffect, useState } from 'react';

type Theme = 'light' | 'dark';

interface ThemeContextType {
theme: Theme;
toggleTheme: () => void;
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>('light');

useEffect(() => {
const stored = localStorage.getItem('theme') as Theme;
if (stored) {
setTheme(stored);
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
setTheme('dark');
}
}, []);

useEffect(() => {
localStorage.setItem('theme', theme);
document.documentElement.classList.toggle('dark', theme === 'dark');
}, [theme]);

const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};

return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}

export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
// app/components/ThemeToggle.tsx
'use client';

import { useTheme } from './ThemeProvider';

export default function ThemeToggle() {
const { theme, toggleTheme } = useTheme();

return (
<button
onClick={toggleTheme}
className="p-2 rounded-lg bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
>
{theme === 'light' ? '🌙' : '☀️'}
</button>
);
}

Styled Components

Installation and Setup

npm install styled-components
npm install -D @types/styled-components
// app/lib/styled-components.ts
'use client';

import styled from 'styled-components';

export const Button = styled.button<{ variant?: 'primary' | 'secondary' }>`
padding: 0.5rem 1rem;
border: none;
border-radius: 0.25rem;
cursor: pointer;
font-weight: 500;
transition: all 0.2s;

${props =>
props.variant === 'primary' &&
`
background-color: #3b82f6;
color: white;

&:hover {
background-color: #2563eb;
}
`}

${props =>
props.variant === 'secondary' &&
`
background-color: #6b7280;
color: white;

&:hover {
background-color: #4b5563;
}
`}
`;

export const Card = styled.div`
background: white;
border-radius: 0.5rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
overflow: hidden;
transition: transform 0.2s;

&:hover {
transform: translateY(-2px);
}
`;

export const CardHeader = styled.div`
padding: 1.5rem;
border-bottom: 1px solid #e5e7eb;
`;

export const CardBody = styled.div`
padding: 1.5rem;
`;

export const Title = styled.h2`
font-size: 1.5rem;
font-weight: 600;
color: #1f2937;
margin-bottom: 0.5rem;
`;

Styled Components with Theme

// app/lib/theme.ts
export const theme = {
colors: {
primary: {
50: '#eff6ff',
500: '#3b82f6',
900: '#1e3a8a',
},
gray: {
50: '#f9fafb',
500: '#6b7280',
900: '#111827',
},
},
fonts: {
sans: 'Inter, system-ui, sans-serif',
mono: 'Fira Code, monospace',
},
spacing: {
xs: '0.5rem',
sm: '1rem',
md: '1.5rem',
lg: '2rem',
xl: '3rem',
},
};

export type Theme = typeof theme;
// app/components/StyledButton.tsx
'use client';

import styled from 'styled-components';
import { theme } from '@/lib/theme';

interface ButtonProps {
variant?: 'primary' | 'secondary';
size?: 'small' | 'large';
}

const StyledButton = styled.button<ButtonProps>`
padding: ${props => props.size === 'small' ? '0.25rem 0.75rem' : props.size === 'large' ? '0.75rem 1.5rem' : '0.5rem 1rem'};
border: none;
border-radius: 0.25rem;
cursor: pointer;
font-weight: 500;
font-family: ${theme.fonts.sans};
transition: all 0.2s;

${props => props.variant === 'primary' && `
background-color: ${theme.colors.primary[500]};
color: white;

&:hover {
background-color: ${theme.colors.primary[900]};
}
`}

${props => props.variant === 'secondary' && `
background-color: ${theme.colors.gray[500]};
color: white;

&:hover {
background-color: ${theme.colors.gray[900]};
}
`}
`;

export default function Button({
children,
variant = 'primary',
size,
...props
}: ButtonProps & { children: React.ReactNode } & React.ButtonHTMLAttributes<HTMLButtonElement>) {
return (
<StyledButton variant={variant} size={size} {...props}>
{children}
</StyledButton>
);
}

CSS-in-JS Solutions

Emotion

npm install @emotion/react @emotion/styled
// app/components/EmotionButton.tsx
'use client';

import { css } from '@emotion/react';
import styled from '@emotion/styled';

const buttonStyles = css`
padding: 0.5rem 1rem;
border: none;
border-radius: 0.25rem;
cursor: pointer;
font-weight: 500;
transition: all 0.2s;
`;

const primaryStyles = css`
background-color: #3b82f6;
color: white;

&:hover {
background-color: #2563eb;
}
`;

export const EmotionButton = styled.button`
${buttonStyles}
${props => props.variant === 'primary' && primaryStyles}
`;

// Usage
export default function ButtonExample() {
return (
<div>
<EmotionButton variant="primary">
Click me
</EmotionButton>
</div>
);
}

Stitches

npm install @stitches/react
// app/lib/stitches.config.ts
import { createStitches } from '@stitches/react';

export const {
styled,
css,
globalCss,
keyframes,
getCssText,
theme,
createTheme,
config,
} = createStitches({
theme: {
colors: {
primary: '#3b82f6',
secondary: '#6b7280',
white: '#ffffff',
gray50: '#f9fafb',
gray900: '#111827',
},
space: {
1: '0.25rem',
2: '0.5rem',
3: '0.75rem',
4: '1rem',
5: '1.25rem',
6: '1.5rem',
},
fonts: {
sans: 'Inter, system-ui, sans-serif',
},
},
media: {
bp1: '(min-width: 640px)',
bp2: '(min-width: 768px)',
bp3: '(min-width: 1024px)',
},
});

export const Button = styled('button', {
padding: '$2 $4',
border: 'none',
borderRadius: '0.25rem',
cursor: 'pointer',
fontWeight: 500,
transition: 'all 0.2s',

variants: {
variant: {
primary: {
backgroundColor: '$primary',
color: '$white',
'&:hover': {
backgroundColor: '#2563eb',
},
},
secondary: {
backgroundColor: '$secondary',
color: '$white',
'&:hover': {
backgroundColor: '#4b5563',
},
},
},
size: {
small: {
padding: '$1 $3',
fontSize: '0.875rem',
},
large: {
padding: '$3 $6',
fontSize: '1.125rem',
},
},
},

defaultVariants: {
variant: 'primary',
},
});

CSS Modules

Setup and Usage

/* app/components/Navigation.module.css */
.nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 2rem;
background-color: white;
border-bottom: 1px solid #e5e7eb;
}

.logo {
font-size: 1.5rem;
font-weight: bold;
color: #1f2937;
}

.links {
display: flex;
gap: 2rem;
}

.link {
color: #6b7280;
text-decoration: none;
font-weight: 500;
transition: color 0.2s;
}

.link:hover {
color: #1f2937;
}

.active {
color: #3b82f6;
}

.mobileMenu {
display: none;
}

@media (max-width: 768px) {
.links {
display: none;
}

.mobileMenu {
display: block;
}
}
// app/components/Navigation.tsx
'use client';

import Link from 'next/link';
import { usePathname } from 'next/navigation';
import styles from './Navigation.module.css';

export default function Navigation() {
const pathname = usePathname();

const links = [
{ href: '/', label: 'Home' },
{ href: '/about', label: 'About' },
{ href: '/blog', label: 'Blog' },
{ href: '/contact', label: 'Contact' },
];

return (
<nav className={styles.nav}>
<div className={styles.logo}>
My App
</div>

<div className={styles.links}>
{links.map(link => (
<Link
key={link.href}
href={link.href}
className={`${styles.link} ${pathname === link.href ? styles.active : ''}`}
>
{link.label}
</Link>
))}
</div>

<button className={styles.mobileMenu}>

</button>
</nav>
);
}

Sass/SCSS

Installation and Setup

npm install -D sass
// app/styles/variables.scss
$primary-color: #3b82f6;
$secondary-color: #6b7280;
$success-color: #10b981;
$error-color: #ef4444;
$warning-color: #f59e0b;

$font-family-sans: Inter, system-ui, sans-serif;
$font-family-mono: 'Fira Code', monospace;

$spacing-xs: 0.25rem;
$spacing-sm: 0.5rem;
$spacing-md: 1rem;
$spacing-lg: 1.5rem;
$spacing-xl: 2rem;

$border-radius: 0.25rem;
$box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);

// Mixins
@mixin button-base {
padding: $spacing-sm $spacing-md;
border: none;
border-radius: $border-radius;
cursor: pointer;
font-weight: 500;
transition: all 0.2s;
}

@mixin button-variant($bg-color, $hover-color) {
background-color: $bg-color;
color: white;

&:hover {
background-color: $hover-color;
}
}

@mixin card {
background: white;
border-radius: $border-radius;
box-shadow: $box-shadow;
overflow: hidden;
}
// app/components/Button.module.scss
@import '../styles/variables';

.button {
@include button-base;

&.primary {
@include button-variant($primary-color, darken($primary-color, 10%));
}

&.secondary {
@include button-variant($secondary-color, darken($secondary-color, 10%));
}

&.success {
@include button-variant($success-color, darken($success-color, 10%));
}

&.small {
padding: $spacing-xs $spacing-sm;
font-size: 0.875rem;
}

&.large {
padding: $spacing-lg $spacing-xl;
font-size: 1.125rem;
}
}

PostCSS and Plugins

PostCSS Configuration

// postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
'postcss-flexbugs-fixes': {},
'postcss-preset-env': {
autoprefixer: {
flexbox: 'no-2009',
},
stage: 3,
features: {
'custom-properties': false,
},
},
},
};

Custom PostCSS Plugin

// postcss.config.js
const customPlugin = () => {
return {
postcssPlugin: 'custom-plugin',
Rule(rule) {
// Custom processing
},
};
};

module.exports = {
plugins: [customPlugin(), require('tailwindcss'), require('autoprefixer')],
};

Animation Libraries

Framer Motion

npm install framer-motion
// app/components/AnimatedCard.tsx
'use client';

import { motion } from 'framer-motion';

interface AnimatedCardProps {
title: string;
content: string;
delay?: number;
}

export default function AnimatedCard({ title, content, delay = 0 }: AnimatedCardProps) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay }}
whileHover={{ scale: 1.05 }}
className="bg-white rounded-lg shadow-md p-6"
>
<h3 className="text-xl font-semibold mb-2">{title}</h3>
<p className="text-gray-600">{content}</p>
</motion.div>
);
}

React Spring

npm install @react-spring/web
// app/components/SpringAnimation.tsx
'use client';

import { useSpring, animated } from '@react-spring/web';

export default function SpringAnimation() {
const styles = useSpring({
from: { opacity: 0, transform: 'translateY(50px)' },
to: { opacity: 1, transform: 'translateY(0px)' },
config: { tension: 280, friction: 60 },
});

return (
<animated.div style={styles} className="bg-blue-500 text-white p-4 rounded">
Animated with React Spring
</animated.div>
);
}

Quick Reference

Styling Approaches

  • Global CSS: App-wide styles in globals.css
  • CSS Modules: Component-scoped styles with .module.css
  • Tailwind CSS: Utility-first CSS framework
  • Styled Components: CSS-in-JS with styled-components
  • Sass/SCSS: CSS preprocessor with variables and mixins

Setup Commands

# Tailwind CSS
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

# Styled Components
npm install styled-components
npm install -D @types/styled-components

# Sass
npm install -D sass

# Animation libraries
npm install framer-motion
npm install @react-spring/web

Best Practices

  • Use CSS Modules for component-specific styles
  • Implement dark mode with CSS variables or Tailwind
  • Use PostCSS for autoprefixing and optimization
  • Consider performance impact of CSS-in-JS
  • Use TypeScript for styled-components props
  • Implement responsive design with mobile-first approach
  • Use CSS custom properties for theming
  • Optimize CSS bundle size in production