Search code examples
javascriptreactjsnext.jstailwind-cssclsx

Can't build className dinamically using clsx and Tailwind


Hi there! Not that-skilled-yet Javascript developer here, using React and Next, more specifically this template

When it comes to declare component class names, I'm using the following utility function, that combines tailwind-merge and clsx, as suggested here:

// utils.ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

To make things easier, improve DRY and even readability, I wish to be able to dynamically insert tailwind modifiers (e.g dark:, hover:, md:...) while declaring these class names, like in the following:

// page.tsx
import { cn, dark, hover, md } from '@/utils'

<Component className={cn(
  "text-white text-sm",
  "w-full h-10",
  hover("font-bold text-lime-500"),
  md(
    "w-1/2 h-20",
    "text-xl"
  )
)}/>

To achieve so, I implemented some other utilities functions:

// utils.ts
function apply_modifier(modifier: string, ...inputs: string[]) {
  return inputs.map((input) => `${modifier}:${input}`).join(" ")
}

function create_specialist_apply_modifier_function(modifier: string) {
  return (...inputs: string[]) => apply_modifier(modifier, ...inputs)
}

const dark = create_specialist_apply_modifier_function("dark")
const hover = create_specialist_apply_modifier_function("hover")
const md = create_specialist_apply_modifier_function("md")
...

I tested it out and I got the string I was expecting every time, however, the results aren't being applied to the component at all, and I couldn't understand why

Even the following won't work:

<Component className={clsx(
    ["text-2xl", "text-white"].map((c) => `dark:${c}`).join(" ")
)}/>

I appreciate any thoughts on understand the problem and also into figure an alternative solution

Obrigado!


Solution

  • As per the documentation:

    The most important implication of how Tailwind extracts class names is that it will only find classes that exist as complete unbroken strings in your source files.

    If you use string interpolation or concatenate partial class names together, Tailwind will not find them and therefore will not generate the corresponding CSS:

    Don’t construct class names dynamically

    <div class="text-{{ error ? 'red' : 'green' }}-600"></div>
    

    In the example above, the strings text-red-600 and text-green-600 do not exist, so Tailwind will not generate those classes.

    Instead, make sure any class names you’re using exist in full:

    Always use complete class names

    <div class="{{ error ? 'text-red-600' : 'text-green-600' }}"></div>
    

    The most straight-forward solution would be to use full class names:

    <Component className={cn(
      "text-white text-sm",
      "w-full h-10",
      "hover:font-bold hover:text-lime-500",
      "md:w-1/2 md:h-20",
      "md:text-xl"
    )}/>
    

    Alternatively, you could safelist classes you'd produce, but this would get unmaintainable fast and you'd have to context-switch a lot between the templates and the Tailwind configuration.

    If you really wanted your functions to work, you could look at transforming content files just before the Tailwind class extraction, such that the transformer converts the function calls to full class names. Note, this only transforms the string content scanned by Tailwind and would not affect the operable code in the project:

    <Component className={cn(
      "text-white text-sm",
      "w-full h-10",
      hover("font-bold text-lime-500"),
      md(
        "w-1/2 h-20",
        "text-xl"
      )
    )}/>
    
    // After some trickery, perhaps something like ↓
    
    <Component className={cn(
      "text-white text-sm",
      "w-full h-10",
      hover("hover:font-bold hover:text-lime-500"),
      md(
        "md:w-1/2 md:h-20",
        "md:text-xl"
      )
    )}/>