Skip to main content

Next.js Custom Components

Introduction

Custom components are the building blocks of Next.js applications. They allow you to split your UI into independent, reusable pieces, making your code more maintainable, readable, and modular. In this tutorial, we'll explore how to create custom components in Next.js, understand their benefits, and see how they fit into the larger Next.js ecosystem.

Custom components in Next.js are essentially React components, but they can leverage Next.js features like built-in CSS support, image optimization, and more. Whether you're building a simple blog or a complex web application, mastering custom components is essential for efficient Next.js development.

Understanding Components in Next.js

Before diving into custom components, let's review what components are in React and Next.js:

  • Components are isolated pieces of UI that can be reused throughout your application
  • They encapsulate their own logic, styling, and structure
  • Components can accept inputs (called "props") and return React elements that describe what should appear on screen
  • Next.js extends React components with additional features and optimizations

Creating Your First Custom Component

Let's start by creating a simple custom component in Next.js. We'll build a Button component that can be reused throughout our application.

First, create a components directory in your Next.js project:

jsx
// components/Button.js
import React from 'react';
import styles from './Button.module.css';

const Button = ({ text, onClick, variant = 'primary' }) => {
return (
<button
className={`${styles.button} ${styles[variant]}`}
onClick={onClick}
>
{text}
</button>
);
};

export default Button;

Now, create a CSS module for styling:

css
/* components/Button.module.css */
.button {
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
transition: background-color 0.3s ease;
}

.primary {
background-color: #0070f3;
color: white;
}

.primary:hover {
background-color: #0051a2;
}

.secondary {
background-color: #f3f3f3;
color: #333;
border: 1px solid #ddd;
}

.secondary:hover {
background-color: #e3e3e3;
}

Using Your Custom Component

Now let's use our custom Button component in a page:

jsx
// pages/index.js
import Button from '../components/Button';

export default function Home() {
const handleClick = () => {
alert('Button clicked!');
};

return (
<div>
<h1>Next.js Custom Components Example</h1>
<Button text="Click me!" onClick={handleClick} />
<Button text="Secondary Button" onClick={handleClick} variant="secondary" />
</div>
);
}

When you run your Next.js application, you'll see two buttons with different styles, and clicking them will trigger the alert.

Creating Composite Components

Real-world applications often need more complex components. Let's create a Card component that combines multiple elements:

jsx
// components/Card.js
import styles from './Card.module.css';
import Button from './Button';

const Card = ({ title, description, buttonText, onButtonClick }) => {
return (
<div className={styles.card}>
<h2 className={styles.title}>{title}</h2>
<p className={styles.description}>{description}</p>
{buttonText && (
<div className={styles.buttonContainer}>
<Button text={buttonText} onClick={onButtonClick} />
</div>
)}
</div>
);
};

export default Card;
css
/* components/Card.module.css */
.card {
border: 1px solid #eaeaea;
border-radius: 10px;
padding: 20px;
margin: 20px 0;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
transition: transform 0.3s ease;
background-color: white;
}

.card:hover {
transform: translateY(-5px);
}

.title {
margin-top: 0;
color: #333;
}

.description {
color: #666;
line-height: 1.5;
}

.buttonContainer {
margin-top: 15px;
}

Using Component Props for Flexibility

Props make components flexible and reusable. Let's use our Card component with different props:

jsx
// pages/products.js
import Card from '../components/Card';

export default function Products() {
const products = [
{ id: 1, title: 'Product 1', description: 'This is the first product.' },
{ id: 2, title: 'Product 2', description: 'This is the second product.' },
{ id: 3, title: 'Product 3', description: 'This is the third product.' }
];

const handleButtonClick = (id) => {
console.log(`Product ${id} added to cart`);
};

return (
<div>
<h1>Products</h1>
{products.map(product => (
<Card
key={product.id}
title={product.title}
description={product.description}
buttonText="Add to Cart"
onButtonClick={() => handleButtonClick(product.id)}
/>
))}
</div>
);
}

Adding Component State

Components can manage their own state. Let's create a Counter component:

jsx
// components/Counter.js
import { useState } from 'react';
import styles from './Counter.module.css';
import Button from './Button';

const Counter = ({ initialValue = 0 }) => {
const [count, setCount] = useState(initialValue);

const increment = () => {
setCount(prevCount => prevCount + 1);
};

const decrement = () => {
setCount(prevCount => prevCount - 1);
};

return (
<div className={styles.counter}>
<h2>Counter: {count}</h2>
<div className={styles.buttonGroup}>
<Button text="Increment" onClick={increment} />
<Button text="Decrement" onClick={decrement} variant="secondary" />
</div>
</div>
);
};

export default Counter;
css
/* components/Counter.module.css */
.counter {
text-align: center;
padding: 20px;
border: 1px solid #eaeaea;
border-radius: 8px;
margin: 20px 0;
}

.buttonGroup {
display: flex;
gap: 10px;
justify-content: center;
margin-top: 15px;
}

Creating a Layout Component

Layout components are special types of components that wrap around pages to provide consistent layout elements like headers, footers, or navigation. Let's create a simple layout component:

jsx
// components/Layout.js
import Head from 'next/head';
import Link from 'next/link';
import styles from './Layout.module.css';

const Layout = ({ children, title = 'Next.js App' }) => {
return (
<div className={styles.container}>
<Head>
<title>{title}</title>
<meta charSet="utf-8" />
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
</Head>
<header className={styles.header}>
<nav>
<Link href="/">
<a className={styles.navLink}>Home</a>
</Link>
<Link href="/products">
<a className={styles.navLink}>Products</a>
</Link>
<Link href="/about">
<a className={styles.navLink}>About</a>
</Link>
</nav>
</header>
<main className={styles.main}>{children}</main>
<footer className={styles.footer}>
<p>&copy; {new Date().getFullYear()} My Next.js App</p>
</footer>
</div>
);
};

export default Layout;
css
/* components/Layout.module.css */
.container {
min-height: 100vh;
display: flex;
flex-direction: column;
}

.header {
background-color: #0070f3;
padding: 1rem;
color: white;
}

.navLink {
color: white;
margin-right: 1rem;
text-decoration: none;
}

.navLink:hover {
text-decoration: underline;
}

.main {
flex: 1;
padding: 2rem;
max-width: 1000px;
margin: 0 auto;
width: 100%;
}

.footer {
background-color: #f3f3f3;
padding: 1rem;
text-align: center;
color: #333;
}

Apply the layout to your page like this:

jsx
// pages/index.js
import Layout from '../components/Layout';
import Button from '../components/Button';
import Counter from '../components/Counter';

export default function Home() {
return (
<Layout title="Home | Next.js Custom Components">
<h1>Welcome to Next.js</h1>
<p>This page demonstrates various custom components.</p>
<Button text="Click me!" onClick={() => alert('Welcome!')} />
<Counter initialValue={5} />
</Layout>
);
}

Component Composition Patterns

Component composition is a powerful pattern in React and Next.js. Let's create a Modal component that uses the children prop to accept content:

jsx
// components/Modal.js
import { useState, useEffect } from 'react';
import styles from './Modal.module.css';
import Button from './Button';

const Modal = ({ isOpen, onClose, title, children }) => {
const [isModalOpen, setIsModalOpen] = useState(isOpen);

useEffect(() => {
setIsModalOpen(isOpen);
}, [isOpen]);

const closeModal = () => {
setIsModalOpen(false);
if (onClose) onClose();
};

if (!isModalOpen) return null;

return (
<div className={styles.modalBackdrop} onClick={closeModal}>
<div
className={styles.modalContent}
onClick={e => e.stopPropagation()}
>
<div className={styles.modalHeader}>
<h3>{title}</h3>
<button className={styles.closeButton} onClick={closeModal}>×</button>
</div>
<div className={styles.modalBody}>
{children}
</div>
<div className={styles.modalFooter}>
<Button text="Close" onClick={closeModal} variant="secondary" />
</div>
</div>
</div>
);
};

export default Modal;
css
/* components/Modal.module.css */
.modalBackdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}

.modalContent {
background-color: white;
border-radius: 8px;
width: 500px;
max-width: 90%;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}

.modalHeader {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
border-bottom: 1px solid #eaeaea;
}

.modalHeader h3 {
margin: 0;
}

.closeButton {
border: none;
background: none;
font-size: 24px;
cursor: pointer;
color: #666;
}

.modalBody {
padding: 20px;
}

.modalFooter {
padding: 15px 20px;
border-top: 1px solid #eaeaea;
text-align: right;
}

Now, we can use the Modal component with different content:

jsx
// pages/with-modal.js
import { useState } from 'react';
import Layout from '../components/Layout';
import Modal from '../components/Modal';
import Button from '../components/Button';

export default function WithModal() {
const [isModalOpen, setIsModalOpen] = useState(false);

return (
<Layout title="Modal Example">
<h1>Modal Component Example</h1>
<Button
text="Open Modal"
onClick={() => setIsModalOpen(true)}
/>

<Modal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
title="Example Modal"
>
<p>This is the content of the modal. You can put any components or text here.</p>
<p>The modal closes when you click outside or the Close button.</p>
</Modal>
</Layout>
);
}

Component Best Practices

When creating custom components in Next.js, follow these best practices:

  1. Keep components focused: Each component should do one thing well
  2. Use TypeScript for type safety: Add type definitions to component props
  3. Optimize for performance: Use React.memo for components that don't change often
  4. Make components reusable: Avoid hardcoding values and use props instead
  5. Organize components logically: Structure folders by feature or component type
  6. Document components: Add comments or use tools like Storybook
  7. Test components: Write unit tests for your components

Here's an example of a component with TypeScript:

tsx
// components/Alert.tsx
import React from 'react';
import styles from './Alert.module.css';

type AlertType = 'success' | 'info' | 'warning' | 'error';

interface AlertProps {
type: AlertType;
message: string;
onClose?: () => void;
}

const Alert: React.FC<AlertProps> = ({ type, message, onClose }) => {
return (
<div className={`${styles.alert} ${styles[type]}`}>
<span className={styles.message}>{message}</span>
{onClose && (
<button className={styles.close} onClick={onClose}>
&times;
</button>
)}
</div>
);
};

export default Alert;

Real-world Application: Product Listing

Let's create a real-world example of a product listing using our custom components:

jsx
// components/ProductList.js
import Card from './Card';
import styles from './ProductList.module.css';

const ProductList = ({ products, onAddToCart }) => {
if (!products || products.length === 0) {
return <p>No products found.</p>;
}

return (
<div className={styles.productGrid}>
{products.map(product => (
<Card
key={product.id}
title={product.name}
description={`$${product.price} - ${product.description}`}
buttonText="Add to Cart"
onButtonClick={() => onAddToCart(product.id)}
/>
))}
</div>
);
};

export default ProductList;
css
/* components/ProductList.module.css */
.productGrid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}

@media (max-width: 768px) {
.productGrid {
grid-template-columns: 1fr;
}
}

Now we can use this component in a page:

jsx
// pages/shop.js
import { useState } from 'react';
import Layout from '../components/Layout';
import ProductList from '../components/ProductList';
import Alert from '../components/Alert';

export default function Shop() {
const [showAlert, setShowAlert] = useState(false);
const [alertMessage, setAlertMessage] = useState('');

const products = [
{ id: 1, name: 'Laptop', price: 999.99, description: 'Powerful laptop for work and gaming' },
{ id: 2, name: 'Smartphone', price: 699.99, description: 'Latest model with advanced camera' },
{ id: 3, name: 'Headphones', price: 149.99, description: 'Noise cancelling wireless headphones' },
{ id: 4, name: 'Tablet', price: 399.99, description: 'Lightweight tablet with HD display' }
];

const handleAddToCart = (id) => {
const product = products.find(p => p.id === id);
setAlertMessage(`Added ${product.name} to cart!`);
setShowAlert(true);
setTimeout(() => setShowAlert(false), 3000);
};

return (
<Layout title="Shop | Next.js Components">
<h1>Product Shop</h1>

{showAlert && (
<Alert
type="success"
message={alertMessage}
onClose={() => setShowAlert(false)}
/>
)}

<ProductList
products={products}
onAddToCart={handleAddToCart}
/>
</Layout>
);
}

Summary

In this tutorial, we've covered:

  • Creating basic custom components in Next.js
  • Styling components with CSS Modules
  • Using props to make components reusable
  • Building composite components that combine simpler ones
  • Adding state to components
  • Creating layout components for consistent page structure
  • Component composition patterns with the children prop
  • Best practices for component development
  • A real-world application using custom components

By mastering custom components in Next.js, you can build modular, maintainable, and reusable UIs for your web applications. Components help you organize your code, promote reusability, and make your application more manageable as it grows.

Additional Resources

To deepen your understanding of Next.js components:

Exercises

  1. Create a Navbar component that highlights the active page
  2. Build a Form component with validation that can be reused across your application
  3. Develop a Carousel component that displays multiple images with navigation controls
  4. Create a theme-switching component that changes your app's color scheme
  5. Build a set of form input components (text input, select, checkbox) with consistent styling and behavior


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)