Multi-prop component using Tailwind CSS & clsx

Posted at 16/07/2023


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

To walk you through it, I'll use a button ⎯ similar to the one I created myself for this website ⎯ as an example but the model is basically 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 one of those tags.

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

Instead of passing in the return call a tag explicitly, we've used the ternary operator to control which one will be rendered. In this case, if you ended 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 that will be common to all instances of the button, regardless of any property. The utility classes I've used for mine are mostly adding text & container-related styles. Note that I haven't written 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

Alright, that's when the fun part starts. Similarly to how you do on Figma, we'll think about which styles we want to have 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, for the colors, I'll use orange (which is my website's primary color), neutral, and white.

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 here, both with the properties and their values. 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: 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/>} />

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 ? 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 specifc get your attention? Anything else? I'd love to hear! Drop me a note somewhere.