Ryan Bethel I am a web developer, electrical engineer, and rock climbing guide from Maine. I like building with React, GraphQL, and JavaScript, but I mostly enjoy finding simple solutions to hard problems. Find me on Twitter @ryanbethel and my blog at ryanbethel.com.

Tailwind CSS tips for creating reusable React components

5 min read 1451

Tailwind Building Components in React

I don’t pretend to be a good designer. But I value good design. Tailwind is the best tool I’ve found to build custom user interfaces that look great.

In this blog post, I’ll share some tips that I’ve found incredibly useful when building reusable React components with Tailwind.

What is Tailwind and why use it?

Tailwind is a utility-first CSS library that exposes CSS properties applied as classes directly to your markup. Before it existed, I used another very popular CSS UI library. It promised beautiful designs with minimal effort. I felt so productive at first throwing buttons, cards, and accordions down as fast as cut and paste. With just a few classes an ugly button turned into a beautiful button.

But the problem with most of these shortcut UI libraries is that their components are basically set in cement. The first time I tried to style a custom date picker with this library I found myself wading through an incomprehensible CSS file. The solution I found was to hack away with important!.

Tailwind requires more work upfront, but it is much easier to build a custom component without full mastery of CSS. Part of this process is to learn to effectively use components with Tailwind.

Getting started with flat markup components

The first key to building good components is to start by not building any. Premature abstractions are hard to back out of. Unless you are sure what the right components will be, it is better to start with flat markup at the level of a page.

Tailwind makes this very easy. You can work quickly in one file and build the structure and style together. The context switching of multiple files adds a lot of friction at the start. When you do break components out, you can keep them in the same file until you actually need to use that component somewhere else. You should only extract it when it becomes difficult to keep it where it is.

How to choose the right API

A confusing API when using a component can lead to bugs. Robin Malfait, who now works for Tailwind Labs, shared his own “unwritten rules for components” in a Discord chat. I share them here because I have not seen a better summary to choosing a good component API.

  1. Be as explicit as possible:
    • Use states instead of booleans. <Button primary disabled secondary active/> has too many invalid permutations. In the explicit version ( <Button state={Button.state.ACTIVE} variant={Button.variant.PRIMARY}/> ), there is no way to be primary and secondary, and no way to be active and disabled
  2. ClassNameas a prop is a code smell:
    • <Button variant={Button.variant.PRIMARY} className="p-4"/> creates an implicit version of your button. It is hard to know what state it is in
  3. Prefer local state over global state:
    • Make it clear where state is coming from
  4. Prefer props over state:
    • It is better to derive from props than to sync with your state
  5. Prefer no props over props:
    • Props increase the possible states your component can be in
  6. Prefer components over props:
    • <Button big fullWidth/> should be <BigButton/> or <FullWidthButton/>
  7. Prefer children over props:
    • <Button iconRight={''}>Apple</Button> should be <Button>Apple </Button>

Below is a badge component written in TypeScript that I use. This is similar to an example that Robin shared in this Gist. This version has four color variations and two sizes.

import React, { ReactNode } from 'react';
import { classNames } from '../util/classNames';
enum Variant {
    RED,
    YELLOW,
    GREEN,
    BLUE,
}
enum Size {
    LARGE,
    SMALL,
}
type Props = {
    variant: Variant;
    children?: ReactNode;
    size: Size;
};
const SIZE_MAPS: Record<Size, string> = {
    [Size.SMALL]: 'px-2.5 text-xs',
    [Size.LARGE]: 'px-3 text-sm',
};
const VARIANT_MAPS: Record<Variant, string> = {
    [Variant.RED]: 'bg-red-100 text-red-800',
    [Variant.YELLOW]: 'bg-yellow-100 text-yellow-800',
    [Variant.GREEN]: 'bg-green-100 text-green-800',
    [Variant.BLUE]: 'bg-blue-100 text-blue-800',
};
export function Badge(props: Props) {
    const { children, variant, size } = props;
    return (
        <span
            className={classNames(
                'inline-flex items-center py-0.5 rounded-full font-medium leading-4 whitespace-no-wrap',
                VARIANT_MAPS[variant],
                SIZE_MAPS[size],
            )}
        >
            {children}
        </span>
    );
}
Badge.defaultProps = {
    variant: Variant.GRAY,
    size: Size.SMALL,
};
Badge.variant = Variant;
Badge.size = Size;

The badge can only be in one of the eight states defined by the enum:
<Badge variant={Badge.variant.RED} size={Badge.size.BIG}>Text</Badge>

We made a custom demo for .
No really. Click here to check it out.

A plain JavaScript version using objects instead of enums to map to the different states is shown below.

import React from 'react';
import { classNames } from '../util/classNames';
const SIZE_MAPS = {
    SMALL: 'px-2.5 text-xs',
    LARGE: 'px-3 text-sm',
};
const VARIANT_MAPS = {
    RED: 'bg-red-100 text-red-800',
    YELLOW: 'bg-yellow-100 text-yellow-800',
    GREEN: 'bg-green-100 text-green-800',
    BLUE: 'bg-blue-100 text-blue-800',
};
export function Badge(props) {
    const { children, variant, size } = props;
    return (
        <span
            className={classNames(
                'inline-flex items-center py-0.5 rounded-full font-medium leading-4 whitespace-no-wrap',
                VARIANT_MAPS[variant],
                SIZE_MAPS[size],
            )}
        >
            {children}
        </span>
    );
}
Badge.variant = VARIANT_MAPS;
Badge.size = SIZE_MAPS;

Bonus: ClassNames Helper

Working with Tailwind in React, and especially building reusable components, often requires combining classname strings conditionally. There are several packages on npm most people use to do this. There’s nothing wrong with that, but to fight the bloat of one more package with dependencies, here is a function Robin also shared to do this. Very clean, not rocket science, but handy.

export function classNames(...classes: (false | null | undefined | string)[]) {
  return classes.filter(Boolean).join(" ");
}
//usage example
<button className={classNames('this is always applied', 
        isTruthy && 'this only when the isTruthy is truthy', 
        active ? 'active classes' : 'inactive classes')}>Text</button>

Types of components in Tailwind

Non-components (templates or snippets)

Sometimes what you are trying to encapsulate is a repeated pattern rather than a component. If the details will be unique for most instances and it does not need to stay linked to the other instances, it’s actually a template or snippet.

A good example of this is what Tailwind released as their paid product, Tailwind UI. These snippets are meant to be cut and pasted into your app to accelerate prototyping. They may become reusable components in your code at some point, assuming you find they are repeated in multiple places in your app with enough consistency to make it clear what the API should be. Otherwise, keep them in your snippet library.

Small components

A button is the classic example of a small component that is often extracted with Tailwind. The example below has 17 utility classes. You might find that you have dozens of buttons across a website that will share 15 out of the 17 classes.

<button type="button" class="inline-flex items-center px-2.5 py-1.5 border 
    border-transparent text-xs font-medium rounded shadow-sm text-white 
    bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 
    focus:ring-offset-2 focus:ring-indigo-500">
  Button text
</button>

In this case, you could use the tailwind @apply directive to combine the common classes above into a new btn class. I usually don’t do this because it is an easier mental model for me to keep most of my components within the component model for React.

Small component (exposing styles)

In some cases, you may want to create a component that encapsulates something other than styles. It may be a click event handler or some other behavior. This is an exception the the “classNames as props” being a code smell. Here, it’s best to use classNames as a prop and put all the Tailwind classes on the component rather than in it.

Large complex component

Large components that include multiple elements, — like an app shell, sidebar, or header — can be tempting to over-parameterize. To make them as configurable as possible, you want to fill them with props. But this makes it harder to read your code and know what state each version of that app shell is in.

Instead, I lean heavily toward the “components over props” and “children over components” end of the API spectrum for these. They often start very similar to one another. But as the site grows and changes, each instance tends to diverge, forcing you to add another prop to accommodate.

Once they are linked, it always seems easier to keep them linked until you suddenly find you have a monster component. The lesson here is: don’t be afraid to break these large components up when they become unwieldy.

Conclusion

Using React is one of the best ways to build modern apps out of components. Tailwind is the easiest way I have found to style apps without high-level mastery of CSS. Together, the two allow you to build reusable components that look and work well together.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Ryan Bethel I am a web developer, electrical engineer, and rock climbing guide from Maine. I like building with React, GraphQL, and JavaScript, but I mostly enjoy finding simple solutions to hard problems. Find me on Twitter @ryanbethel and my blog at ryanbethel.com.

Leave a Reply