Styled Components For Tailwind?!

While working on a recent side project, I had the idea to build a simple API like styled components that works with Tailwind. The idea is given a list of class names, create a component that you can easily share without writing all the normal boilerplate.

The goal is something that looks like this:

const Text = styled.p("text-sans text-lg font-light")

What appeals to me about this is in one line I have a reusable component with automatic prop types inferred from the tag I selected, and class names are automatically merged if you add a className prop when using the component.

Let’s see the implementation of this styled function!

import { createElement } from "react"
import { twMerge } from "tailwind-merge"

const elements = [
  "a",
  "button",
  "div",
  "p",
  // See the link below for the full list of properties
  // https://gist.github.com/mskelton/61ea38abcae6473b9cafe777b6a2ad60#file-styled-ts-L4-L117
] as const

type SupportedHTMLElements = (typeof elements)[number]

type StyledComponent<T extends SupportedHTMLElements> = (
  classes: string,
) => {
  (props: React.ComponentPropsWithoutRef<T>): JSX.Element
}

type Styled = {
  <T extends SupportedHTMLElements>(tag: T): StyledComponent<T>
} & {
  [T in SupportedHTMLElements]: StyledComponent<T>
}

/** Create a styled component for the given HTML tag. */
export const styled = (<T extends SupportedHTMLElements>(tag: T) =>
  (classes) => {
    return function StyledComponent({ children, className, ...props }) {
      return createElement(
        tag,
        {
          className: className ? twMerge(classes, className) : classes,
          ...props,
        },
        children,
      )
    }
  }) as Styled

// Add the shorthand methods for each element we support (e.g., styled.p, styled.div, etc.)
for (const element of elements) {
  styled[element] = styled(element)
}