Multi-prop component using Tailwind CSS & clsx

Posted on 16/07/2023

Last updated on 26/12/2023


Say you want to create a button with many custom properties such as variant, color, width, etc., and each one has values that make sense to you. How would you do that? Well, it may sound silly in this day & age, but I just learned how to do that using Tailwind CSS and another interesting clsx.

To walk you through it, I'll use a button ⎯ similar to the one I created for this website ⎯ as an example, but the model is the same for all sorts of other components.


Don't forget to install clsx

Make sure to install clsx by running npm install --save clsx on your project before starting to follow this guide.

Creating the foundational component

Buttons can render, under the hood, either a button or a tag, depending on the use case. Let's kick this off by creating a generic `Component` that can be either of those tags.

export default function Button({ children, ...props }) { let Component = props.href ? Link : "button"; return <Component className={className} {...props}>{children}</Component>; }

Instead of explicitly passing a tag in the return call, we've used the ternary operator to control which one gets rendered. In this case, if you end up passing an href prop, it will render as a <Link/> component (which is a "native" Next.js component). You could also just use <a> if you're not using Next.js. Then, otherwise, it will render as a button.

Additionally, we have also used the JSX spread operator {...props} so our button can receive the many other properties we'll create for it, and similarly with className in there.

Designing the base styles

Now that the foundational structure is all set, we can write the styles common to all button instances, regardless of any property. The utility classes I've used for mine are adding text & container-related styles. Note that I have yet to write any color and size-based classes, as I want to have props for those.

className = "flex items-center justify-center rounded-full transition-all focus:outline-none focus:ring-2 focus:ring-orange-600 text-[13px] tracking-[0.2px] leading-none cursor-pointer";

Composing the properties

Similar to what you'd do on Figma, we'll think about which styles we want configurable in our button through props. You could say we're designing the "component API" by doing this.

For example, two super common props we'd find in most component libraries out there are variant and color . For the former, I got inspired by Joy UI's global variants and thought about solid, soft, outline, and plain. Hopefully, you can picture how they look from the names. And then, I'll use orange (which is my website's primary color), neutral, and white, for the colors,.

To create those, we'll put all of them inside a constant, like so:

const variantStyles = { solid: "bg-orange-500 hover:bg-orange-800", soft: "bg-orange-50 ring-1 ring-orange-400/60 hover:bg-orange-100 dark:bg-orange-500/10 dark:ring-1 dark:ring-inset dark:ring-orange-400/20 dark:hover:bg-orange-400/20 dark:hover:ring-orange-700", outline: "ring-1 ring-inset ring-gray-200 hover:bg-gray-100 hover:ring-gray-300 dark:ring-white/10 dark:hover:bg-white/5", plain: "hover:bg-gray-100 dark:hover:bg-white/5", }; const colorStyle = { orange: "text-orange-600 dark:text-orange-400", neutral: "text-gray-800 dark:text-gray-400", white: "text-white", };

Those are the specific utility classes I'm using for the button you see throughout this website. But feel free to get crazy with the properties and their values here. For instance, I've also added props for font weight and width for my buttons.

Passing the props to the component

With the above done, that's when we start to use the clsx package (don't forget to install it!) to render all of those strings in the class slot. We'll pass all of them close to the base styles we've defined earlier and assign an actual name we want each prop to have. Additionally, we also need to get the props and pass them inside the button's curly brackets while also declaring which are their default values (i.e. how the component will look if you use it without explicitly configuring it with any prop).

import Link from "next/link"; import clsx from "clsx"; const variantStyles = { solid: "bg-orange-500 hover:bg-orange-800", soft: "bg-orange-50 ring-1 ring-orange-400/60 hover:bg-orange-100 dark:bg-orange-500/10 dark:ring-1 dark:ring-inset dark:ring-orange-400/20 dark:hover:bg-orange-400/20 dark:hover:ring-orange-700", outline: "ring-1 ring-inset ring-gray-200 hover:bg-gray-100 hover:ring-gray-300 dark:ring-white/10 dark:hover:bg-white/5", plain: "hover:bg-gray-100 dark:hover:bg-white/5", }; const colorStyle = { orange: "text-orange-600 dark:text-orange-400", neutral: "text-gray-800 dark:text-gray-400", white: "text-white", }; export default function Button({ variant = "primary", color = "orange", className, children, ...props }) { let Component = props.href ? Link : "button"; className = clsx( "flex items-center justify-center rounded-full transition-all focus:outline-none focus:ring-2 focus:ring-orange-600 text-[13px] tracking-[0.2px] leading-none cursor-pointer", variantStyles[variant], colorStyle[color], className ); return ( <Component className={className} {...props}> {children} </Component> ); }

That's it! At this point, you should be able to add your button component somewhere and pick a variant and a color like so.

<Button variant="soft" color="neutral"/>

Bonus #1: Adding icons props

To get a bit crazier, let's add props that allow you to render an icon and control its positioning inside the button. We can actually have two icons, one rendered on the left and another on the right, so let's do that by first adding them inside the component markup, like so:

<Component className={className} {...props}> {icon === "left" && iconSvg} {children} {icon === "right" && iconSvg} </Component>

Note that we're using two new props: icon and iconSvg. Let's add them to the list like so:

import Link from "next/link"; import clsx from "clsx"; const variantStyles = { solid: "bg-orange-500 hover:bg-orange-800", soft: "bg-orange-50 ring-1 ring-orange-400/60 hover:bg-orange-100 dark:bg-orange-500/10 dark:ring-1 dark:ring-inset dark:ring-orange-400/20 dark:hover:bg-orange-400/20 dark:hover:ring-orange-700", outline: "ring-1 ring-inset ring-gray-200 hover:bg-gray-100 hover:ring-gray-300 dark:ring-white/10 dark:hover:bg-white/5", plain: "hover:bg-gray-100 dark:hover:bg-white/5", }; const colorStyle = { orange: "text-orange-600 dark:text-orange-400", neutral: "text-gray-800 dark:text-gray-400", white: "text-white", }; export default function Button({ variant = "primary", color = "orange", icon, iconSvg, className, children, ...props }) { let Component = props.href ? Link : "button"; className = clsx( "flex items-center justify-center rounded-full transition-all focus:outline-none focus:ring-2 focus:ring-orange-600 text-[13px] tracking-[0.2px] leading-none cursor-pointer", variantStyles[variant], colorStyle[color], className ); return ( <Component className={className} {...props}> {children} </Component> ); }

Now, whenever you want to have, say, a chevron icon render on the right side of the button's text, this is how you'd do it:

<Button icon="right" iconSvg={<ChevronIcon/>} />

Bonus #2: Extending the link capacities

I recently added a "Jump to content" link on my website's navbar, which is a handy accessibility tool, to help folks navigating through the site with a keyboard to get to the main content faster, skipping the entire navigation links.

To pull that off, I extended the Button's link capacities so that I can choose whether it renders as a Next.js Link component or as a plain HTML anchor tag. Here's the code for that below:

let Component = props.href ? (props.external ? "a" : Link) : "button";

So now, if I pass the external prop, together with href, the Button is rendered as an anchor tag!

Closing thoughts

We're done with our custom-designed button, which has custom props exactly the way any designer would do these days on Figma. To add even more props that control all sorts of different styles, you'd do the exact same process. That's it, we have it! Hope you have learned something ⎯ I sure had my occasional dev-alike head-blown moment with this.

Entire code snippet

Lastly, here's the whole code for the button component I've created for my website.

/Button.js
import Link from "next/link"; import clsx from "clsx"; function ArrowIcon(props) { return ( <svg viewBox="0 0 20 20" fill="none" aria-hidden="true" {...props}> <path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" d="m11.5 6.5 3 3.5m0 0-3 3.5m3-3.5h-9" /> </svg> ); } const variantStyles = { solid: "bg-orange-500 hover:bg-orange-800", soft: "bg-orange-50 ring-1 ring-orange-400/60 hover:bg-orange-100 dark:bg-orange-500/10 dark:ring-1 dark:ring-inset dark:ring-orange-400/20 dark:hover:bg-orange-400/20 dark:hover:ring-orange-700", outline: "ring-1 ring-inset ring-gray-200 hover:bg-gray-100 hover:ring-gray-300 dark:ring-white/10 dark:hover:bg-white/5", plain: "hover:bg-gray-100 dark:hover:bg-white/5", }; const colorStyle = { orange: "text-orange-600 dark:text-orange-400", neutral: "text-gray-800 dark:text-gray-400", white: "text-white", }; const fontWeights = { semibold: "font-semibold", medium: "font-medium", }; const padding = { large: "px-4 py-3", medium: "px-3 py-2 ", small: "px-2 py-1 ", zero: "p-0", }; const widthStyle = { fit: "w-fit", full: "w-full", }; export default function Button({ variant = "primary", color = "orange", weight = "medium", p = "medium", width = "fit", className, children, arrow, icon, iconSvg, ...props }) { let Component = props.href ? (props.external ? "a" : Link) : "button"; className = clsx( "flex items-center justify-center rounded-full transition-all focus:outline-none focus:ring-2 focus:ring-orange-600 text-[13px] tracking-[0.2px] leading-none cursor-pointer", variantStyles[variant], colorStyle[color], fontWeights[weight], padding[p], widthStyle[width], className ); let arrowIcon = ( <ArrowIcon className={clsx( "w-6 h-6 ml-0.5 transition-all group-hover:translate-x-0.5", variant === "text" && "relative top-px", arrow === "left" && "-ml-1 rotate-180", arrow === "right" && "" )} /> ); return ( <Component className={className} {...props}> {icon === "left" && iconSvg} {arrow === "left" && arrowIcon} {children} {arrow === "right" && arrowIcon} {icon === "right" && iconSvg} </Component> ); }

Thoughts about this article?

I'm all ears for feedback! Typos? Did something specific get your attention? Anything else? I'd love to hear! Drop me a note somewhere.