Frontend developer focused on inclusive design

Create components with Next.js and Tailwind

I built a simple website with Next.js and Tailwind under the hood.

This post serves as a collection of notes I made to demonstrate how I built components for my website.

Preparation

To reduce code repetition and to keep the project clean and tidy in the future, I decided to develop reusable React components.

Note, these components are styled using Tailwind. Sometimes, I use custom global styles to style elements inside the component.

To prepare for developing reusable components:

  1. Open a project with your website in Visual Studio Code, or any other preferable code editor.
  2. Under a root directory of the project, create a components directory. It will contain all reusable components of a website.

The name of this directory could be anything. However, it makes sense to stick with more meaningful file structure names. It makes easier to understand the structure of a web project.

Also, before creating components, review structure of your website in order to find sections that might are shared between pages. If you have such sections then consider to create a component of each section.

Create components

Components are great. They allow to deconstruct a page into a series of blocks. Then, these blocks can be reused between pages.

As a result, it helps to keep a project organized, which also simplifies the process of maintenance of the project's codebase.

Below you can find a documented instructions of developing reusable React components, that I made for my website with Next.js and Tailwind.

Container component

This component is responsible for outputting the main container, which holds all sections of the website. Sections are added to the container as child elements: children.

To create the component:

  1. In components directory, create a new file and name it Container.js.
  2. Add the component code (see below) to this newly created file.
  3. Read comments in the code snippet for additional information about the component and its functionality.

Here is an example of a final code snippet of the component:

/**
 * Container component.
 *
 * This component is designed
 * to wrap child elements within a container.
 *
 * @param {JSX.Element} children Child elements.
 */
const Container = ({children}) => (
    <div className="sm:my-20 sm:mx-auto m-2 mb-10 sm:p-8 p-4 sm:border-4 border-2 border-solid border-gray-100 sm:space-y-8 space-y-5 bg-gray-50 shadow-2xl rounded-lg max-w-2xl">
        {children}
    </div>
);

export default Container;

Header component

This component is responsible for outputting header section, with an included SEO functionality, which comes from next-seo plugin.

Here is a final code snippet of this component:

// Functionality of the Header component.

/**
 * Head component.
 *
 * This component is a part of Next.js, and
 * it adds elements to the `<head>` of the webpage.
 *
 * @link https://nextjs.org/docs/api-reference/next/head
 */
import Head from 'next/head'

/**
 * Link component.
 *
 * This component is a part of Next.js, and
 * it helps to link Next.js pages.
 *
 * @link https://nextjs.org/docs/api-reference/next/link
 */
import Link from 'next/link'

/**
 * NextSeo component.
 *
 * This component is a part of Next SEO plugin, and
 * it renders out the tags in the `<head>`,
 * needed for SEO.
 *
 * @link https://github.com/garmeeh/next-seo
 */
import { NextSeo } from 'next-seo';

/**
 * Navigation component.
 *
 * This component outputs
 * the list of main website links.
 */
import Navigation from '../components/Navigation';

/**
 * Header component.
 *
 * This component outputs
 * the header area of the website.
 *
 * @param {object} props Properties of the current component.
 *
 * @see Head
 * @see NextSeo
 * @see Link
 * @see Navigation
 */
const Header = (props) => (
    <>
        <Head>
            <link rel="icon" href="/favicon.ico" />
            <meta name="theme-color" content="#e2e8f0" />
        </Head>

        <NextSeo
            title={props.seo.title}
            description={props.seo.description}
        />

        <header>
            <div className="flex items-center border-b sm:pb-8 pb-5">
                <div className="font-bold text-xs uppercase leading-tight tracking-wider border-solid border-b border-r border-gray-300 bg-gray-200 py-2 px-9 -ml-10 rounded shadow-sm">
                    <Link href="/">
                        <a className="no-underline text-gray-700 hover:text-gray-500">
                            Taras Dashkevych
                        </a>
                    </Link>
                </div>
                <p className="text-sm text-gray-600 max-w-xs text-right ml-auto sm:pl-2 pl-4">
                    Font-end developer focused on inclusive design
                </p>
            </div>

            <Navigation />
        </header>
    </>
);

export default Header;

This component is responsible for outputting a navigation menu, with links to various pages on the website. Also, this component highlights the current active page on the website.

Here is an example of a final code snippet of the component:

// Functionality of the Navigation component.

/**
 * Link component.
 *
 * This component is a part of Next.js, and
 * it helps to link Next.js pages.
 *
 * @link https://nextjs.org/docs/api-reference/next/link
 */
import Link from 'next/link';

/**
 * useRouter hook.
 *
 * This hook is a part of Next.js, and
 * it helps to access to route object.
 *
 * @link https://nextjs.org/docs/api-reference/next/router
 */
import { useRouter } from 'next/router';

/**
 * Availible website links.
 *
 * This is an array of objects, each
 * object has `label` and `path`.
 */
const links = [
    {
        label: 'About',
        path: '/'
    },
    {
        label: 'Blog',
        path: '/blog',
    },
    {
        label: 'Contact',
        path: '/contact',
    },
];

/**
 * Navigation Link component.
 *
 * This component outputs a label for the menu link.
 *
 * @param {object} link A link with path and label.
 * @param {string} currentPath Path of the current page.
 */
const NavigationLink = ({ link, currentPath }) => {
    // Avoid displaying a menu item with a link for the current page.
    if ( currentPath === link.path ) {
        return (
            <span className="block rounded-l rounded-r border-l-4 border-r-4 border-gray-300 px-2 font-bold text-black leading-none">
                {link.label}
            </span>
        )
    }

    // For other pages, display a menu item with a link.
    return (
        <Link href={link.path}>
            <a className="no-underline hover:underline leading-none block text-gray-600 hover:text-black">{link.label}</a>
        </Link>
    )
}

/**
 * Navigation component.
 *
 * This is an array of objects, each
 * object has `label` and `path`.
 *
 * @see NavigationLink
 */
const Navigation = () => {
    const router = useRouter();

    return (
        <nav className="main-navigation mt-4">
            <ul className="flex sm:gap-8 gap-6 uppercase text-xs sm:tracking-wider">
                {
                    links.map((link, index) => {
                        return (
                            <li key={index}>
                                <NavigationLink link={link} currentPath={router.pathname} />
                            </li>
                        )
                    })
                }
            </ul>
        </nav>
    );
}

export default Navigation;

Page title component

This component is responsible for outputting the main title of a page. See a reason of using Level 1 heading for a page title.

To create the component:

  1. In components directory, create a new file and name it PageTitle.js.
  2. Add the component code (see below) to this newly created file.
  3. Read comments in the code snippet for additional information about the component and its functionality.

Here is an example of a final code snippet of the component:

// Functionality of the Page title component.

/**
 * Page title component.
 *
 * This component outputs markup
 * for the main title of a page.
 *
 * Note, the page title is added to the component
 * as a child element. So, it is also possible to add
 * other HTML elements to the component.
 *
 * @param {JSX.Element} children Child elements.
 */
 const PageTitle = ({children}) => (
	<h1 className="font-bold sm:text-2xl text-xl">
		{children}
	</h1>
);

export default PageTitle;

This component is responsible for outputting the footer area of the site. It also imports other components to be displayed in the footer area.

To create the component:

  1. In components directory, create a new file and name it Footer.js.
  2. Add the component code (see below) to this newly created file.
  3. Read comments in the code snippet for additional information about the component and its functionality.

Here is an example of a final code snippet of the component:

// Functionality of the Footer component.

/**
 * SocialLinks component.
 *
 * This component outputs
 * the list of social links.
 */
import SocialLinks from '../components/SocialLinks';

/**
 * Footer component.
 *
 * This component outputs
 * the footer area of the website.
 *
 * @see SocialLinks
 */
const Footer = () => (
	<footer className="border-t sm:pt-6 pt-4">
		<SocialLinks />
	</footer>
);

export default Footer;

This component is responsible for outputting a list of social links. Each list item is consisted of a link which includes icon and label.

Here is an example of a final code snippet of the component:

// Functionality of the Social links component.

/**
 * Icon component.
 *
 * This component outputs
 * SVG icon based on set `id` and `size`.
 */
import Icon from '../components/Icon';

/**
 * Availible social links.
 *
 * This is a multidimensional object, where each
 * object has `id`, `url`, `label`.
 */
const links = {
    'twitter': {
        url: 'https://twitter.com/TarasDashkevych',
        label: 'Twitter'
    },
    'youtube': {
        url: 'https://www.youtube.com/channel/UC47RU4f10bXjxGTEFfpOPxA',
        label: 'YouTube'
    },
    'instagram': {
        url: 'https://www.instagram.com/taras.codes',
        label: 'Instagram'
    },
}

/**
 * SocialLinks component.
 *
 * This component outputs
 * the list of social links.
 *
 * @see Icon
 */
const SocialLinks = () => {
    let jsx = [];

    for ( const [id, link] of Object.entries(links) ) {
        jsx.push(
            <li className={ id + '-icon' } key={id}>
                <a className="flex items-center gap-2" href={link.url} target="_blank">
                    <Icon id={id} size={24} />
                    <span className="sm:not-sr-only sr-only">{link.label}</span>
                </a>
            </li>
        );
    }

    return (
        <ul className="flex sm:justify-start justify-between gap-6 uppercase text-xs tracking-wider">
            {jsx}
        </ul>
    )
}

export default SocialLinks;

Icon component

This component is responsible for outputting SVG icon, based on set arguments:

  • id: unique id of the icon.
  • size: width and height of the icon.

Here is an example of a final code snippet of the component:

// Functionality of the Icon component.

/**
 * Icon component.
 *
 * This component outputs
 * the list of social links.
 *
 * @param {string} id Unique identifier of the icon.
 * @param {integer} size Width and height of the icon.
 */
const Icon = ({ id, size }) => {
    switch (id) {
        case 'twitter':
            return (
                <svg role="img" viewBox="0 0 24 24" width={size} height={size} xmlns="http://www.w3.org/2000/svg">
                    <path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/>
                </svg>
            );
        case 'youtube':
            return (
                <svg role="img" viewBox="0 0 24 24" width={size} height={size} xmlns="http://www.w3.org/2000/svg">
                    <path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/>
                </svg>
            );
        case 'instagram':
            return (
                <svg role="img" viewBox="0 0 24 24" width={size} height={size} xmlns="http://www.w3.org/2000/svg">
                    <path d="M12 0C8.74 0 8.333.015 7.053.072 5.775.132 4.905.333 4.14.63c-.789.306-1.459.717-2.126 1.384S.935 3.35.63 4.14C.333 4.905.131 5.775.072 7.053.012 8.333 0 8.74 0 12s.015 3.667.072 4.947c.06 1.277.261 2.148.558 2.913.306.788.717 1.459 1.384 2.126.667.666 1.336 1.079 2.126 1.384.766.296 1.636.499 2.913.558C8.333 23.988 8.74 24 12 24s3.667-.015 4.947-.072c1.277-.06 2.148-.262 2.913-.558.788-.306 1.459-.718 2.126-1.384.666-.667 1.079-1.335 1.384-2.126.296-.765.499-1.636.558-2.913.06-1.28.072-1.687.072-4.947s-.015-3.667-.072-4.947c-.06-1.277-.262-2.149-.558-2.913-.306-.789-.718-1.459-1.384-2.126C21.319 1.347 20.651.935 19.86.63c-.765-.297-1.636-.499-2.913-.558C15.667.012 15.26 0 12 0zm0 2.16c3.203 0 3.585.016 4.85.071 1.17.055 1.805.249 2.227.415.562.217.96.477 1.382.896.419.42.679.819.896 1.381.164.422.36 1.057.413 2.227.057 1.266.07 1.646.07 4.85s-.015 3.585-.074 4.85c-.061 1.17-.256 1.805-.421 2.227-.224.562-.479.96-.899 1.382-.419.419-.824.679-1.38.896-.42.164-1.065.36-2.235.413-1.274.057-1.649.07-4.859.07-3.211 0-3.586-.015-4.859-.074-1.171-.061-1.816-.256-2.236-.421-.569-.224-.96-.479-1.379-.899-.421-.419-.69-.824-.9-1.38-.165-.42-.359-1.065-.42-2.235-.045-1.26-.061-1.649-.061-4.844 0-3.196.016-3.586.061-4.861.061-1.17.255-1.814.42-2.234.21-.57.479-.96.9-1.381.419-.419.81-.689 1.379-.898.42-.166 1.051-.361 2.221-.421 1.275-.045 1.65-.06 4.859-.06l.045.03zm0 3.678c-3.405 0-6.162 2.76-6.162 6.162 0 3.405 2.76 6.162 6.162 6.162 3.405 0 6.162-2.76 6.162-6.162 0-3.405-2.76-6.162-6.162-6.162zM12 16c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4zm7.846-10.405c0 .795-.646 1.44-1.44 1.44-.795 0-1.44-.646-1.44-1.44 0-.794.646-1.439 1.44-1.439.793-.001 1.44.645 1.44 1.439z"/>
                </svg>
            );
        case 'volume-up':
            return (
                <svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} fill="none" viewBox="0 0 24 24" stroke="currentColor">
                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z" />
                </svg>
            );
        case 'badge-check':
            return (
                <svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} fill="none" viewBox="0 0 24 24" stroke="currentColor">
                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" />
                </svg>
            );
        case 'exclamation-circle':
            return (
                <svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 20 20" fill="currentColor">
                    <path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
                </svg>
            );
    }
}

export default Icon;

To extend a list of available icons in this component, simpty add an additional case code block inside switch.

Beautifly made SVG icons can be found in this library, which provides a free icon copies with JSX format. Just remember to set width={size} and height={size}, and also remove className attribute from the copied icon.

Conclusion

Creation of each component has teach me something new about React. Hopefully, you found something useful in my notes on how to develop components with Next.js and Tailwind.