Skip to content
Before dive in
Introduction

Introduction


tailwindest banner

Tailwindest is a lightweight and fast className generation library only for tailwindcss. With type safety, maintainable class names, and maximized reusable style designs, you can make more robust products.

Classname generator

The fact that tailwindest is a string generator gives it the following advantages.

  1. Tiny bundle size, 768B
  2. Independency of framework and environment
  3. Zero compile steps

Elegant conditional styling

The easiest way to understand conditional styling is to think of switches. We're surrounded by them all the time: we turn lights on and off, and our beloved keyboards are all switches. Conditional styling can also be thought of as a type of switch (albeit a physical one).

Toggle switch

The simplest switch is a toggle, which can only express two states: on/off. Let's use this concept in programming. You need to create a button that supports dark/light mode, i.e and its style should change depending on the value of a boolean variable called isDarkMode. It's like a light switch: pressing it (on = true) turns it on (light mode), pressing the other side (off = false) turns it off (dark mode).

toggle concept

const themeBtn = tw.toggle({
    base: {}, //    base style definition
    truthy: {}, //  dark mode style
    falsy: {}, //   light mode style
})
const ThemeBtn = ({ children }) => {
    const [isDarkMode, setIsDarkMode] = useState(false)
 
    return <button className={themeBtn.class(isDarkMode)}>{children}</button>
}

Rotary switch

But in the real world, we can't express everything with a toggle switch, so we need to create a switch that's a step more complex: a rotary switch. If you've ever used a microwave oven, it's easy to understand! From steps 1, 2 to 10, we can fine-tune the time and intensity we want with a rotary switch. So, for example, if we want to present a UI with 4 states: default, success, failure and pending, we can import the concept of a rotary switch: if it's success, we can associate a style for success, if it's failure, we can associate a style for failure, and so on.

rotary concept

const button = tw.rotary({
    base: {}, //    base style definition
    default: {}, // default style
    success: {}, // success style
    error: {}, //   error style
    pending: {}, // pending style
})
const Button = ({
    children,
    type = "default",
}: {
    children: ReactNode
    type: GetVariants<typeof btn>
}) => {
    return <button className={button.class(type)}>{children}</button>
}

Combination of rotary, variants

However, there may be times when we need a few more states. In other words, we may need to create multiple rotary switches to represent more complex states. This is where the variants comes in.

variants are combinations of rotary switches - think of the microwave at home. You'll notice that it has a few rotary switches, such as time and an intensity.

microwave

Similarly, with variants, you first create a category of switches for each of the criteria you need to adjust, and then just combine them.

variants concept

const button = tw.variants({
    base: {},
    variants: {
        type: {
            default: {},
            success: {},
            error: {},
            pending: {},
        },
        size: {
            sm: {},
            md: {},
            lg: {},
        },
    },
})
interface BtnProps extends GetVariants<typeof btn> {
    children: ReactNode
}
const Btn = ({ children, type = "default", size = "md" }) => (
    <button
        className={btn.class({
            type,
            size,
        })}
    >
        {children}
    </button>
)

Pros and cons

Pros

  1. Type-safety
  2. Readability
  3. Reusability
  4. Maintainability

Cons

  1. Takes a little longer to write style
  2. Verbose than original tailwind

Conclusion

The great thing about tailwind is that it makes it quick and easy to get your product done. But it introduces new problems: readability and maintainability. In contrast, tailwindest can't get your product done any faster than regular tailwind. It specializes in designing products that are easy to refactor and robust style.

Now just combine the best of both worlds

💡

if your className is too long to maintain, write it with tailwindest, but if it's short and easy to read, write an inline className.

If you apply tailwindest in the right circumstances, you can build a much more robust product with tailwind.

Real world adaptation

  1. Short className: inline-class
  2. Complex & Reusable className: tailwindest

The code below is the Card component in the Feature on the main page.

Original

export const Card = ({
    onClick,
    children,
    icon,
}: {
    onClick: () => void
    icon: ReactNode
    children: ReactNode
}) => {
    return (
        <button
            type="button"
            onClick={onClick}
            className="flex flex-col items-start justify-between gap-2 p-2 hover:translate-y-[1.5px] hover:border-transparent hover:opacity-100 active:border-amber-400/10 active:opacity-75 transition-all ease-in duration-75 select-none sm:flex-row sm:items-center sm:p-2.5 bg-amber-600/10 rounded border-amber-400/20 border"
        >
            <div className="w-6 h-6 min-w-[1.5rem] min-h-[1.5rem] p-1 bg-gradient-to-bl from-amber-700/30 to-amber-700/50 border-amber-400 border rounded md:w-7 md:h-7 md:min-w-[1.75rem] md:min-h-[1.75rem] md:p-1.5">
                {icon}
            </div>
 
            <div className="font-bold text-sm md:text-base md:font-semibold text-start">
                {children}
            </div>
        </button>
    )
}

Build with tailwindest

import { tw } from "~/tw"
 
// 💡 Reusable className
export const cardContainer = tw.style({
    backgroundColor: "bg-amber-600/10",
 
    borderRadius: "rounded",
    borderColor: "border-amber-400/20",
    borderWidth: "border",
})
 
// 💡 Long className
const card = tw
    .style({
        display: "flex",
        flexDirection: "flex-col",
        alignItems: "items-start",
        justifyContent: "justify-between",
        gap: "gap-2",
 
        padding: "p-2",
 
        ":hover": {
            transformTranslateY: "hover:translate-y-[1.5px]",
            borderColor: "hover:border-transparent",
            opacity: "hover:opacity-100",
        },
        ":active": {
            borderColor: "active:border-amber-400/10",
            opacity: "active:opacity-75",
        },
        transition: "transition-all ease-in",
        transitionDuration: "duration-75",
        userSelect: "select-none",
 
        "@sm": {
            flexDirection: "sm:flex-row",
            alignItems: "sm:items-center",
            padding: "sm:p-2.5",
        },
    })
    .compose(cardContainer.style)
 
// 💡 Long className
const cardIcon = tw.style({
    width: "w-6",
    height: "h-6",
    minWidth: "min-w-[1.5rem]",
    minHeight: "min-h-[1.5rem]",
 
    padding: "p-1",
 
    backgroundImage: "bg-gradient-to-bl",
    backgroundImageGradientStart: "from-amber-700/30",
    backgroundImageGradientEnd: "to-amber-700/50",
 
    borderColor: "border-amber-400",
    borderWidth: "border",
    borderRadius: "rounded",
 
    "@md": {
        width: "md:w-7",
        height: "md:h-7",
        minWidth: "md:min-w-[1.75rem]",
        minHeight: "md:min-h-[1.75rem]",
 
        padding: "md:p-1.5",
    },
})
 
export const Card = ({
    onClick,
    children,
    icon,
}: {
    onClick: () => void
    icon: ReactNode
    children: ReactNode
}) => {
    return (
        <button type="button" onClick={onClick} className={card.class}>
            <div className={cardIcon.class}>{icon}</div>
            {/* ⚡️ Short and don't have to reuse it */}
            <div className="font-bold text-sm md:text-base md:font-semibold text-start">
                {children}
            </div>
        </button>
    )
}

How need it?

  1. Wants type-safe tailwind
  2. Wants full type definition of tailwind
  3. Wants great conditional styling DX, like cva & stitches