Multi-prop component using Tailwind CSS & clsx

Published on 16/07/2023

Last updated on 28/03/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 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.

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";
3  
4  return <Component className={className} {...props}>{children}</Component>;
5}

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="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".

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: "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",
4  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",
5  plain: "hover:bg-gray-100 dark:hover:bg-white/5",
6  };
7    
8const colors = {
9  orange: "text-orange-600 dark:text-orange-400",
10  neutral: "text-gray-800 dark:text-gray-400",
11  white: "text-white",
12};

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 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";
3
4const variants = {
5  solid: "bg-orange-500 hover:bg-orange-800",
6  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",
7  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",
8  plain: "hover:bg-gray-100 dark:hover:bg-white/5",
9};
10  
11const colors = {
12  orange: "text-orange-600 dark:text-orange-400",
13  neutral: "text-gray-800 dark:text-gray-400",
14  white: "text-white",
15};
16
17export default function Button({
18  variant = "primary",
19  color = "orange",
20  className,
21  children,
22  ...props
23  }) {
24  let Component = props.href ? Link : "button";
25
26  className = clsx(
27          "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",
28  variants[variant],
29  colors[color],
30  className
31  );
32
33  return (
34    <Component className={className} {...props}>
35      {children}
36    </Component>
37  );
38}

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";
3
4const variants = {
5  solid: "bg-orange-500 hover:bg-orange-800",
6  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",
7  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",
8  plain: "hover:bg-gray-100 dark:hover:bg-white/5",
9};
10  
11const colors = {
12  orange: "text-orange-600 dark:text-orange-400",
13  neutral: "text-gray-800 dark:text-gray-400",
14  white: "text-white",
15};
16
17export default function Button({
18  variant = "primary",
19  color = "orange",
20  icon, 
21  iconSvg,
22  className,
23  children,
24  ...props
25  }) {
26    let Component = props.href ? Link : "button";
27
28    className = clsx(
29    "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",
30    variants[variant],
31    colors[color],
32    className
33    );
34
35  return (
36    <Component className={className} {...props}>
37      {children}
38    </Component>
39  );
40}

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

Solid
Soft
Outline
Plain
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";
3
4function ArrowIcon(props) {
5return (
6  <svg viewBox="0 0 20 20" fill="none" aria-hidden="true" {...props}>
7    <path
8    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}
16
17const variants = {
18  solid: "bg-orange-500 hover:bg-orange-800",
19  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",
20  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",
21  plain: "hover:bg-gray-100 dark:hover:bg-white/5",
22};
23
24const colors = {
25  orange: "text-orange-600 dark:text-orange-400",
26  neutral: "text-gray-800 dark:text-gray-400",
27  white: "text-white",
28};
29
30const fontWeights = {
31  semibold: "font-semibold",
32  medium: "font-medium",
33};
34
35const padding = {
36  large: "px-4 py-3",
37  medium: "px-3 py-2 ",
38  small: "px-2 py-1 ",
39  zero: "p-0",
40};
41
42const widthStyle = {
43  fit: "w-fit",
44  full: "w-full",
45};
46
47export default function Button({
48  variant = "primary",
49  color = "orange",
50  weight = "medium",
51  p = "medium",
52  width = "fit",
53  className,
54  children,
55  arrow,
56  icon,
57  iconSvg,
58  ...props
59  }) {
60
61    let Component = props.href
62    ? props.external
63    ? "a"
64    : Link
65    : props.button
66    ? "button"
67    : "div";
68
69    className = clsx(
70      "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",
71    variants[variant],
72    colors[color],
73    fontWeights[weight],
74    padding[p],
75    widthStyle[width],
76    className
77    );
78
79    let arrowIcon = (
80    <ArrowIcon
81      className={clsx(
82      "w-6 h-6 ml-0.5 transition-all group-hover:translate-x-0.5",
83      variant === "text" && "relative top-px",
84      arrow === "left" && "-ml-1 rotate-180",
85      arrow === "right" && ""
86      )}
87    />
88  );
89
90  return (
91    <Component className={className} {...props}>
92      {icon === "left" && iconSvg}
93      {arrow === "left" && arrowIcon}
94      {children}
95      {arrow === "right" && arrowIcon}
96      {icon === "right" && iconSvg}
97    </Component>
98  );
99}