A Deep Dive into Reusable component , Conditional and Dynamic Styling in Nextjs

Chukwudi Nweze is a Frontend Engineer with a strong focus on web performance optimization and user experience. He writes about strategies that make modern web apps faster and more efficient.
Introduction
Imagine you have been assigned the task of developing a user interface that integrates buttons and icons of varying sizes. Your objective is to duplicate a given button or icon into three distinct sizes—small, medium, and large. Furthermore, each size variant must be accessible in three different colour schemes.
Traditionally, this process involves manually generating buttons in green, red and yellow for each of the three size categories. While this approach accomplishes the desired outcome, it quickly leads to a codebase that is both cumbersome and difficult to maintain. This is due to the need to duplicate code for every combination of button size and colour.
Overview
This tutorial exclusively focuses on introducing you to three different libraries that simplify dynamic and conditional styling in Next.js. By the end of this tutorial, you will have a comprehensive understanding of reusable components, and conditional and dynamic styling in Next.js, having successfully built the user interface shown below.

Prerequisites
To successfully follow and complete this tutorial, you should have basic knowledge of Typescript, Tailwind, and Nextjs.
Setting up the application
Follow the steps below to set up the starter code on your local machine. Note that header component has already been created, as it's not part of the primary focus of this tutorial.
git clone https://github.com/chukwudinweze/nextjs-conditional-and-dynamic-styling-starter-code.git
# cd into the directory
cd nextjs-conditional-and-dynamic-styling-starter-code
# install dependencies
yarn install
or
npm install
Once installed, your project directory should look like this:
📦app
┣ 📜favicon.ico
┣ 📜globals.css
┣ 📜layout.tsx
┗ 📜page.tsx
📦components
┣ 📂header
┃ ┣ 📜header.tsx
┃ ┗ 📜theme-toggle.tsx
┗ 📜logo.tsx
📦public
┣ 📜next.svg
┗ 📜vercel.svg
┣ 📜.eslintrc.json
┣ 📜.gitignore
┣ 📜next-env.d.ts
┣ 📜next.config.js
┣ 📜package-lock.json
┣ 📜package.json
┣ 📜postcss.config.js
┣ 📜README.md
┣ 📜tailwind.config.ts
┗ 📜tsconfig.json
To launch the application, run the following command, depending on the package manager of your choice:
If you used yarn as your package manager, run:
yarn dev
If you used yarn as your package manager, run:
npm run dev

Open localhost:3000 in your browser to view the app.

If you're curious about how the header left-bottom-border-radius was created, feel free to explore this tool.
Let's get started
Navigation
Let's commence with the navbar. Consider the image below, which illustrates three distinct components within the navbar, each featuring an input field and an associated icon or button. The objective is to develop a single component capable of being reused for each of the three components highlighted below.

To achieve this, navigate to your project directory, create components/nav/nav.tsx as shown below:
// components/nav/nav.tsx
📦components
┣ 📂header
┃ ┣ 📜header.tsx
┃ ┗ 📜theme-toggle.tsx
┣ 📂nav
┃ ┗ 📜nav.tsx
┗ 📜logo.tsx
Paste the code below:
import { BiMap, BiSearch } from "react-icons/bi";
import { MdOutlineCheckBoxOutlineBlank } from "react-icons/md";
import NavItem from "./nav-item";
const Nav = () => {
return (
<nav className=" flex justify-between lg:mx-20 divide-x-2 shadow-sm p-1 py-3 md:translate-y-[-50%] bg-white rounded-sm overflow-x-auto">
<NavItem
icon={BiSearch}
inputType="text"
placeholder="filter by size, companies or expertise..."
/>
<NavItem
icon={BiMap}
inputType="text"
placeholder="filter by location..."
/>
<NavItem inputType="check" />
</nav>
);
};
export default Nav;
As illustrated in the code above, we will create a navItem—a reusable component that accepts three props: icon, inputType, and placeholder.
The navItem is designed to render an icon and a text input if the inputType prop is set to "text" and to render a checkbox, a paragraph, and a button if the inputType is set to "check."

Notice that the first and second navItem only renders an icon and a text input, while the third component renders a checkbox, a paragraph and a button.
To create our navItem, navigate to the project directory and create components/nav/nav-item.tsx as illustrated below:
📦components
┣ 📂header
┃ ┣ 📜header.tsx
┃ ┗ 📜theme-toggle.tsx
┣ 📂nav
┃ ┣ 📜nav-item.tsx
┃ ┗ 📜nav.tsx
┗ 📜logo.tsx
Paste the code below:
// components/nav/nav-item.tsx
import { IconType } from "react-icons";
interface NavItemProps {
icon?: IconType;
inputType: "text" | "check";
placeholder?: string;
}
// Accepts 3 props(icon, inputType and placeholder)
const NavItem = ({ icon: Icon, inputType, placeholder }: NavItemProps) => {
return (
<div className="flex justify-between flex-1 items-center px-2">
{inputType === "text" && Icon && <Icon />}
{inputType === "text" && (
<input
type="text"
placeholder={placeholder}
className="p-1 flex-1 placeholder:text-xs"
/>
)}
{inputType === "check" && (
<input
type="checkbox"
placeholder="filter by size, companies or expertise"
/>
)}
{inputType === "check" && <p>Full Time Only</p>}
{inputType === "check" && <button>Search</button>}
</div>
);
};
export default NavItem;

Currently, the buttons and icons lack styling. We will revisit and apply styling once we create our dynamic icon badge and button component.
Main Section
In the main section, we have 12 components that share a similar structure.

Once again, let's create another reusable component titled jobCard that renders an icon and a div element, as outlined above.
Creating a Job Card
In your project directory, create components/jobCard.tsx as shown below:
📦components
┣ 📂header
┃ ┣ 📜header.tsx
┃ ┗ 📜theme-toggle.tsx
┣ 📂nav
┃ ┣ 📜nav-item.tsx
┃ ┗ 📜nav.tsx
┣ 📜jobCard.tsx
┗ 📜logo.tsx
Paste the code below:
// components/jobCard.tsx
import { IconType } from "react-icons";
import { TbPointFilled } from "react-icons/tb";
// Define the props interface for the JobCard component
interface JobCardProps {
type: string;
title: string;
company: string;
location: string;
time: string;
logo: IconType;
}
// Define the JobCard component
const JobCard = ({
company,
location,
title,
type,
time,
logo: Logo,
}: JobCardProps) => {
return (
// Outer container with styling
<div className="bg-white space-y-8 py-5 px-5 relative">
<div className="space-y-2">
{/* Displaying the logo at the top with absolute positioning */}
<span className="absolute top-0 transform -translate-y-1/2">
<Logo />
</span>
{/* Displaying time, type, job title, and company information */}
<div className="text-xs text-gray-400 flex items-center space-x-1 font-semibold">
<p>{time}</p>
<TbPointFilled size={8} />
<p>{type}</p>
</div>
<p className="font-bold text-black text-sm">{title}</p>
<p className="text-gray-400 text-xs font-semibold">{company}</p>
</div>
{/* Displaying the job location */}
<p className="text-brandColor font-bold text-sm">{location}</p>
</div>
);
};
// Export the JobCard
export default JobCard;
Navigate to the app/page and modify as shown below:
import { Button } from "@/components/button";
import JobCard from "@/components/jobCard";
import { jobs } from "@/data";
const HomePage = () => {
return (
<div>
<div className="flex justify-center mt-11">
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-3 xl:grid-cols-3 gap-10 border w-full">
{jobs.map((job) => (
<JobCard
key={job.title}
company={job.company}
title={job.title}
time={job.time}
location={job.location}
logo={job.logo}
logoBg={job.logoBgColor}
type={job.type}
/>
))}
</div>
</div>
<div className="text-center w-28 mx-auto mt-8">
<button>Load more</button>
</div>
</div>
);
};
export default HomePage;

The next task is to style the icons in the JobCard.
Libraries we will be using
We need to create an icon component that renders dynamically based on its component props such as color, size, and background. To accomplish this, we will install three packages: clsx, tailmerge, and cva. Explanations for each package will be provided as we proceed.
Install CLSX:
yarn add tailwind-merge,
or
npm install tailwind-merge
Clsx will be used to apply classes to our components based on certain criteria. Given that we intend to dynamically style our icon component, we will use clsx to apply classes to the icon component based on its prop values. Refer here for a quick guide on clsx.
Install Tailwind-merge:
yarn add tailwind-merge
or
npm install tailwind-merge
Tailwind-merge is a utility function that allows the merging of tailwind CSS classes without having to worry about style conflicts.
In contrast to standard CSS behaviour, which checks for specificity, Tailwind-merge ensures that the most recent CSS property takes precedence in the event of conflicting classes.
This will be particularly useful for merging and overriding any conflicting classes that may arise in the icon component. Refer here for a quick guide on clsx.
Install CVA:
yarn add cva
or
npm install cva
We will use CVA to create and assign styles for each of the icon variants we will define, such as default, small icon, big icon and so on. CVA will enable us to define distinct icon variants by associating specific CSS classes with each variant. If this sounds complicated, hold on—once you see it in action, you will understand it better. You can learn more here.
Building a dynamic icon component
Having installed the three dependencies required to create our dynamic icon. Navigate to the project directory and create components/icon-badge.tsx file
Paste the code below:
// Import necessary libraries and components
import { cva, type VariantProps } from "class-variance-authority";
import { type ClassValue, clsx } from "clsx";
import { IconType } from "react-icons";
import { BiHomeAlt2 } from "react-icons/bi";
import { twMerge } from "tailwind-merge";
// Define icon styling variants using class-variance-authority
const iconVariant = cva("rounded-md flex items-center justify-center", {
variants: {
bgColor: {
// Background color variants
default: "bg-transparent",
black: "bg-black",
purple: "bg-brandColor",
darkBlue: "bg-blue-950",
},
iconColor: {
// Icon color variants
default: "text-brandColor",
white: "text-white",
black: "text-black",
},
bgSize: {
// Background size variants
default: "p-1",
sm: "p-[2px]",
lg: "p-[3px]",
},
iconSize: {
// Icon size variants
default: "h-8 w-8",
sm: "h-16 w-16",
lg: "h-20 w-20",
},
},
// Default variants in case not provided
defaultVariants: {
bgColor: "default",
iconColor: "default",
bgSize: "default",
iconSize: "default",
},
});
// Define types for variant props
type IconVariantProps = VariantProps<typeof iconVariant>;
// Define the props interface for the IconBadge component
interface IconBadgeProps extends IconVariantProps {
icon: IconType;
}
// Define the IconBadge component
export const IconBadge = ({
icon: Icon,
bgColor,
iconColor,
bgSize,
iconSize,
}: IconBadgeProps) => {
// Apply styling classes using tailwind-merge and clsx
return (
<div className={twMerge(clsx(iconVariant({ bgColor, bgSize })))}>
<Icon className={twMerge(clsx(iconVariant({ iconColor, iconSize })))} />
</div>
);
};
We have defined the required variants for our icon within the
iconVariantobject. Notice that each styling variant in theiconVariantobject has a default style.When using the
IconBadge, if a variant is not explicitly specified, the default style associated with that variant is automatically applied.IconVariantPropstype is created usingVariantProps<IconVariantType>, which captures the shape of theiconVariantobject, including its properties and their types.The
IconBadgePropsinterface extendsIconVariantPropsand adds aniconproperty of typeIconType. This means thatIconBadgeexpects to receive not only the styling variants but also aniconprop.
Using our IconBadge
Navigate to components/jobCard and paste the code within the span as shown below:
<span className="absolute top-0 transform -translate-y-1/2">
{/* <Logo/> */}
<IconBadge
bgColor="darkBlue"
iconColor="white"
bgSize="sm"
iconSize="sm"
icon={logo}
/>
</span>
The icon in our job card now dynamically renders based on the specified variant props provided in the code above.

Revisiting the Navitems

Remember, we have not yet styled the icons in nav-item. To style the icons, navigate to the components/nav-items and make the following modifications as shown below:
import { IconType } from "react-icons";
import { IconBadge } from "../icon-badge";
interface NavItemProps {
icon?: IconType;
inputType: "text" | "check";
placeholder?: string;
}
const NavItem = ({ icon, inputType, placeholder }: NavItemProps) => {
return (
<div className="flex justify-between flex-1 items-center px-2">
{/* modify as shown below */}
{inputType === "text" && icon && (
<IconBadge icon={icon!} bgSize="sm" />
)}
{inputType === "text" && (
<input
type="text"
placeholder={placeholder}
className="p-1 flex-1 placeholder:text-xs"
/>
)}
{inputType === "check" && (
<input
type="checkbox"
placeholder="filter by size, companies or expertise"
/>
)}
{inputType === "check" && <p>Full Time Only</p>}
{inputType === "check" && <button>Search</button>}
</div>
);
};
export default NavItem;
We only provided the bgSize and the icon prop in the code modification. This means that default variants will be applied for other unspecified variant props, such iconColor.

Building a dynamic button component
Let's style the search button in the navbar as well as the load more button in the footer. The goal is to create another dynamic button and reuse it in both places: navItem and footer.
Navigate to the project directory, create components/button.tsx and paste the code below:
// components/button
import { cva, type VariantProps } from "class-variance-authority";
import clsx from "clsx";
import { ReactNode } from "react";
import { twMerge } from "tailwind-merge";
// Define styling variants for the Button
const buttonVariant = cva("rounded-md flex items-center justify-center", {
variants: {
// variant for the background color
variant: {
default: ["bg-white", "text-black"],
purple: ["bg-brandColor", "text-white"],
},
// Variant for the button's size
size: {
default: ["py-2", "px-4", "text-sm"],
sm: ["py-3", "px-5", "text-lg"],
lg: ["py-5", "px-5", "text-lg"],
},
},
// Default values for the variants
defaultVariants: {
variant: "default",
size: "default",
},
});
// Define the prop types for the Button component using VariantProps
type ButtonVariantProps = VariantProps<typeof buttonVariant>;
// Define prop types for the Button extending ButtonVariantProps
interface ButtonProps extends ButtonVariantProps {
children: ReactNode; // Children can be any ReactNode
}
export const Button = ({ size, variant, children }: ButtonProps) => {
return (
// Render a button with dynamically applied styling using twMerge and clsx
<button className={twMerge(clsx(buttonVariant({ variant, size })))}>
{children}
</button>
);
};
Just like in components/icon-badge.tsx, we have defined different variants for our app button.
Now, navigate to components/nav-item.tsx and modify the search button as shown below.
// components/nav-item.tsx
import { IconType } from "react-icons";
import { IconBadge } from "../icon-badge";
import { Button } from "../button";
interface NavItemProps {
icon?: IconType;
inputType: "text" | "check";
placeholder?: string;
}
const NavItem = ({ icon, inputType, placeholder }: NavItemProps) => {
return (
<div className="flex justify-between flex-1 items-center px-2">
{/*---- modify icon below ----*/}
{inputType === "text" && icon && <IconBadge icon={icon!} bgSize="sm" />}
{inputType === "text" && (
<input
type="text"
placeholder={placeholder}
className="p-1 flex-1 placeholder:text-xs"
/>
)}
{inputType === "check" && (
<input
type="checkbox"
placeholder="filter by size, companies or expertise"
/>
)}
{inputType === "check" && <p>Full Time Only</p>}
{/*---- modify search button below -----*/}
{inputType === "check" && <Button variant="purple">Search</Button>}
</div>
);
};
export default NavItem;

Finally, navigate to the app/page.tsx and modify the button as shown below
// app/page.tsx
import { Button } from "@/components/button";
import JobCard from "@/components/jobCard";
import { jobs } from "@/data";
const HomePage = () => {
return (
<div>
<div className="flex justify-center mt-11">
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-3 xl:grid-cols-3 gap-10 border w-full">
{jobs.map((job) => (
<JobCard
key={job.title}
company={job.company}
title={job.title}
time={job.time}
location={job.location}
logo={job.logo}
logoBg={job.logoBgColor}
type={job.type}
/>
))}
</div>
</div>
<div className="text-center w-28 mx-auto mt-8">
{/*Modify as shown below*/}
<Button variant="purple">Load more</Button>
</div>
</div>
);
};
export default HomePage;

Conclusion
We used clsx, cva, and tailwind-merge to streamline the creation of dynamic and conditionally styled components. This approach not only provides easy component customization but also ensures scalability and reusability. We also learned how to use the class variant authority(cva) to define different variants of our reusable component. I truly hope you enjoyed building the job portal.
Level up your skills in nextjs by checking out my last article - Prisma ORM with MongoDB in Nextjs

