Multi-prop component using Tailwind CSS & clsx
Published on 16/07/2023
Last updated on 02/11/2024
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 neat package called clsx, that we'll use to merge class strings together and also improve our code's readibility.
To walk you through it, I'll use a button-similar to the ones you see throughout my website-as an example, but the model is the same for all sorts of other components.
Installing clsx
Run the following command to install cslx:
1npm install --save clsx
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.
1export default function Button({ children, ...props }) {2 let Component = props.href ? Link : "button";34return <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.
1className={clsx(2 "text-[0.8125rem] tracking-[0.2px] leading-none",3 "flex items-center justify-center rounded-full",4 "cursor-pointer select-none transition-all",5 "focus:outline-none focus:ring-2 focus:ring-orange-600"})
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".
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:
1const variants = {2 solid: "bg-orange-500 hover:bg-orange-800",3 soft: clsx(4 "bg-orange-50 ring-1 ring-orange-400/60 hover:bg-orange-100",5 "dark:bg-orange-500/10 dark:hover:bg-orange-400/20",6 "dark:ring-inset dark:ring-orange-400/20 dark:hover:ring-orange-700",7 ),8 outline: clsx(9 "hover:bg-gray-100 dark:hover:bg-white/5"10 "ring-1 ring-inset ring-gray-200 dark:ring-white/10 hover:ring-gray-300",11 ),12 plain: "hover:bg-gray-100 dark:hover:bg-white/5",13};1415const colors = {16 orange: "text-orange-600 dark:text-orange-400",17 neutral: "text-gray-800 dark:text-gray-400",18 white: "text-white",19};
Those are the specific utility classes I'm using for the buttons you see throughout this website. But feel free to get crazy with the properties and their values here. For instance, I've also added other props for controlling font-weight and width.
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 merge all of those strings in the class attribute. 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 pass them inside the button's curly brackets while declaring which are their default values (i.e., how the component will look if you use it without explicitly inserting any prop).
1import Link from "next/link";2import clsx from "clsx";34const variants = {5 solid: "bg-orange-500 hover:bg-orange-800",6 soft: clsx(7 "bg-orange-50 ring-1 ring-orange-400/60 hover:bg-orange-100",8 "dark:bg-orange-500/10 dark:hover:bg-orange-400/20",9 "dark:ring-inset dark:ring-orange-400/20 dark:hover:ring-orange-700",10 ),11 outline: clsx(12 "hover:bg-gray-100 dark:hover:bg-white/5"13 "ring-1 ring-inset ring-gray-200 dark:ring-white/10 hover:ring-gray-300",14 ),15 plain: "hover:bg-gray-100 dark:hover:bg-white/5",16};1718const colors = {19 orange: "text-orange-600 dark:text-orange-400",20 neutral: "text-gray-800 dark:text-gray-400",21 white: "text-white",22};2324export default function Button({25 variant = "plain",26 color = "orange",27 className,28 children,29 ...props30 }) {31 let Component = props.href ? Link : "button";3233 className = clsx(34 "text-[0.8125rem] tracking-[0.2px] leading-none",35 "flex items-center justify-center rounded-full",36 "cursor-pointer select-none transition-all",37 "focus:outline-none focus:ring-2 focus:ring-orange-600",38 variants[variant],39 colors[color],40 className41 );4243 return (44 <Component className={className} {...props}>45 {children}46 </Component>47 );48}
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:
1<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, adding them inside the component markup, like so:
1<Component className={className} {...props}>2 {icon === "left" && iconSvg}3 {children}4 {icon === "right" && iconSvg}5</Component>
Note that we're using two new props: icon
and iconSvg
.
Let's add them to the list like so:
1import Link from "next/link";2import clsx from "clsx";34const variants = {5 solid: "bg-orange-500 hover:bg-orange-800",6 soft: clsx(7 "bg-orange-50 ring-1 ring-orange-400/60 hover:bg-orange-100",8 "dark:bg-orange-500/10 dark:hover:bg-orange-400/20",9 "dark:ring-inset dark:ring-orange-400/20 dark:hover:ring-orange-700",10 ),11 outline: clsx(12 "hover:bg-gray-100 dark:hover:bg-white/5"13 "ring-1 ring-inset ring-gray-200 dark:ring-white/10 hover:ring-gray-300",14 ),15 plain: "hover:bg-gray-100 dark:hover:bg-white/5",16};1718const colors = {19 orange: "text-orange-600 dark:text-orange-400",20 neutral: "text-gray-800 dark:text-gray-400",21 white: "text-white",22};2324export default function Button({25 variant = "plain",26 color = "orange",27 icon,28 iconSvg,29 className,30 children,31 ...props32 }) {33 let Component = props.href ? Link : "button";3435 className = clsx(36 "text-[0.8125rem] tracking-[0.2px] leading-none",37 "flex items-center justify-center rounded-full",38 "cursor-pointer select-none transition-all",39 "focus:outline-none focus:ring-2 focus:ring-orange-600",40 variants[variant],41 colors[color],42 className43 );4445 return (46 <Component className={className} {...props}>47 {children}48 </Component>49 );50}
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:
1<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 navigate through the site with a keyboard so they 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 control whether it renders as a Next.js Link
component or as a plain HTML anchor tag. Here's the code for that below:
1let Component = props.href ? props.external ? "a" : Link : "button";
Wrap up
To wrap up the article, here's the whole code for the button component. Hope you learned something useful!
Button demo
1<Button variant="solid">Button</Button>2<Button variant="soft">Button</Button>3<Button variant="outline">Button</Button>4<Button variant="plain">Button</Button>
Full code snippet
1import Link from "next/link";2import clsx from "clsx";34function ArrowIcon(props) {5 return (6 <svg viewBox="0 0 20 20" fill="none" aria-hidden="true" {...props}>7 <path8 stroke="currentColor"9 strokeLinecap="round"10 strokeLinejoin="round"11 d="m11.5 6.5 3 3.5m0 0-3 3.5m3-3.5h-9"12 />13 </svg>14 );15}1617const variants = {18 solid: "bg-orange-500 hover:bg-orange-800",19 soft: clsx(20 "bg-orange-50 ring-1 ring-orange-400/60 hover:bg-orange-100",21 "dark:bg-orange-500/10 dark:hover:bg-orange-400/20",22 "dark:ring-inset dark:ring-orange-400/20 dark:hover:ring-orange-700",23 ),24 outline: clsx(25 "hover:bg-gray-100 dark:hover:bg-white/5"26 "ring-1 ring-inset ring-gray-200 dark:ring-white/10 hover:ring-gray-300",27 ),28 plain: "hover:bg-gray-100 dark:hover:bg-white/5",29};3031const colors = {32 orange: "text-orange-600 dark:text-orange-400",33 neutral: "text-gray-800 dark:text-gray-400",34 white: "text-white",35};3637const fontWeights = {38 semibold: "font-semibold",39 medium: "font-medium",40};4142const padding = {43 large: "px-4 py-3",44 medium: "px-3 py-2 ",45 small: "px-2 py-1 ",46 zero: "p-0",47};4849const widthStyle = {50 fit: "w-fit",51 full: "w-full",52};5354export default function Button({55 variant = "plain",56 color = "orange",57 weight = "medium",58 p = "medium",59 width = "fit",60 className,61 children,62 arrow,63 icon,64 iconSvg,65 ...props66 }) {67 let Component = props.href68 ? props.external69 ? "a"70 : Link71 : props.button72 ? "button"73 : "div";7475 className = clsx(76 "text-[0.8125rem] tracking-[0.2px] leading-none",77 "flex items-center justify-center rounded-full",78 "cursor-pointer select-none transition-all",79 "focus:outline-none focus:ring-2 focus:ring-orange-600",80 variants[variant],81 colors[color],82 fontWeights[weight],83 padding[p],84 widthStyle[width],85 className86 );8788 let arrowIcon = (89 <ArrowIcon90 className={clsx(91 "w-6 h-6 ml-0.5 transition-all group-hover:translate-x-0.5",92 variant === "text" && "relative top-px",93 arrow === "left" && "-ml-1 rotate-180",94 arrow === "right" && ""95 )}96 />97 );9899 return (100 <Component className={className} {...props}>101 {icon === "left" && iconSvg}102 {arrow === "left" && arrowIcon}103 {children}104 {arrow === "right" && arrowIcon}105 {icon === "right" && iconSvg}106 </Component>107 );108}