Frivolous Concatenation
I see this pattern in a lot of Tailwind code.
tsxclassName ={cn ("flex items-center gap-1.5","px-4 py-2.5","typography-body-sm text-neu-700 dark:text-neu-600 whitespace-pre",)}
tsxclassName ={cn ("flex items-center gap-1.5","px-4 py-2.5","typography-body-sm text-neu-700 dark:text-neu-600 whitespace-pre",)}
Please don’t do this.
I get it. Human minds yearn for order. The Soulless imitate our weakness.
Just write it together. One string.
tsxclassName ="typography-body-sm flex items-center gap-1.5 whitespace-pre px-4 py-2.5 text-neu-700 dark:text-neu-600"
tsxclassName ="typography-body-sm flex items-center gap-1.5 whitespace-pre px-4 py-2.5 text-neu-700 dark:text-neu-600"
I understand the motivation to split it, especially in more complex production
scenarios, like in shadcn:
tsxclassName ={cn ("hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex","in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize","[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize","hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full","[[data-side=left][data-collapsible=offcanvas]_&]:-right-2","[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",className ,)}
tsxclassName ={cn ("hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex","in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize","[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize","hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full","[[data-side=left][data-collapsible=offcanvas]_&]:-right-2","[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",className ,)}
There is some readability gain, but it’s not worth the things we lose. I’m not
even talking about the performance cost. Even if we use the most basic cn
function like xs.filter(Boolean).join(' '), no tailwind-merge. We lose
predictability and greppability.
Predictability: The Prettier plugin for Tailwind CSS can order classes in long strings, so we always get the same order. Just trust the formatter. Predictable is nice.
Greppability: If there’s no concatenation, even for mapping from props to style, we can copy a class from the browser and immediately jump to the code.
I always rebind project search, so it’s just:
And I’m right where I wanna be.
But what if…
But what if the concatenation isn’t needless? If we actually have some state or
props, such as variant or size, that we’re mapping to styles.
tsxclassName ={cn ("w-(--sidebar-width) fixed inset-y-0 z-10 hidden h-svh transition-[left,right,width] duration-200 ease-linear md:flex",side === "left"? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]": "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",// Adjust the padding for floating and inset variants.variant === "floating" ||variant === "inset"? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]": "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",className ,)}
tsxclassName ={cn ("w-(--sidebar-width) fixed inset-y-0 z-10 hidden h-svh transition-[left,right,width] duration-200 ease-linear md:flex",side === "left"? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]": "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",// Adjust the padding for floating and inset variants.variant === "floating" ||variant === "inset"? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]": "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",className ,)}
I’m not a radical here. I like using aria- attributes for state and I’ve used
data- attributes for props like variant.
tsx"relative flex items-center justify-center gap-2.5 font-normal text-base/none text-neu-0 bg-neu-900 hover:bg-neu-800 active:bg-neu-700 font-sans h-14 px-8 data-[size=md]:h-12 data-[variant=secondary]:bg-neu-100 dark:data-[variant=secondary]:bg-neu-100/80 dark:data-[variant=secondary]:hover:bg-neu-200/50 data-[variant=secondary]:text-neu-900 data-[variant=secondary]:hover:bg-neu-200/75 data-[variant=secondary]:active:bg-neu-200/90 data-[variant=tertiary]:bg-neu-100 data-[variant=tertiary]:text-neu-900 data-[variant=tertiary]:hover:bg-neu-200 data-[variant=tertiary]:active:bg-neu-300 gql-focus-visible [aria-disabled]:bg-neu-800 aria-disabled:pointer-events-none dark:data-[variant=tertiary]:bg-neu-900/10 dark:data-[variant=tertiary]:text-neu-900 dark:data-[variant=tertiary]:hover:bg-neu-900/[.125] dark:data-[variant=tertiary]:active:bg-neu-800/20 dark:data-[variant=tertiary]:ring-1 dark:data-[variant=tertiary]:ring-inset dark:data-[variant=tertiary]:ring-neu-900/20",
tsx"relative flex items-center justify-center gap-2.5 font-normal text-base/none text-neu-0 bg-neu-900 hover:bg-neu-800 active:bg-neu-700 font-sans h-14 px-8 data-[size=md]:h-12 data-[variant=secondary]:bg-neu-100 dark:data-[variant=secondary]:bg-neu-100/80 dark:data-[variant=secondary]:hover:bg-neu-200/50 data-[variant=secondary]:text-neu-900 data-[variant=secondary]:hover:bg-neu-200/75 data-[variant=secondary]:active:bg-neu-200/90 data-[variant=tertiary]:bg-neu-100 data-[variant=tertiary]:text-neu-900 data-[variant=tertiary]:hover:bg-neu-200 data-[variant=tertiary]:active:bg-neu-300 gql-focus-visible [aria-disabled]:bg-neu-800 aria-disabled:pointer-events-none dark:data-[variant=tertiary]:bg-neu-900/10 dark:data-[variant=tertiary]:text-neu-900 dark:data-[variant=tertiary]:hover:bg-neu-900/[.125] dark:data-[variant=tertiary]:active:bg-neu-800/20 dark:data-[variant=tertiary]:ring-1 dark:data-[variant=tertiary]:ring-inset dark:data-[variant=tertiary]:ring-neu-900/20",
In the code above, I obvi started with two variants, and only later needed a
tertiary. I can acknowledge that this is getting to the point where I
should use cva. We don’t really need the
greppability on the class either, because looking at the component, you can
guess it’s called Button.
Still, it’s fine. It’s not even close to my ugliest Tailwind code. I’m not gonna refactor it until it grows to the point of being unbearable.
We still put too much focus on readability and not enough on perf, ease of finding, and ease of working with the code.
Instead, we can embrace ugly and leverage the tools we have to make it more bearable.
Augment yourself. We live in Cyberpunk, right?
Ask the clanker to parse it if you really can’t look at it. Set
"editor.wordWrap": "on" or equivalent in your text editor, grab some
extensions like
Tailwind Fold
or
Tailwind CSS Highlight.
Don’t change the runtime just for something as subjective as readability or aesthetics.
Don’t go overboard and write everything in one huge string always: cn is a
great function. class-variance-authority is amazing. We should respect them
and not reach for them instantly and everywhere.
The AI reviewing this wants me to add a rule of thumb, something constructive to finish the post with.
Nah. Trust your gut. And git gud, man up or woman up, or both.
Also don’t panic, we’ve been frivolous and sloppy for a long time. Phil Pearl
wrote
a parallel post
about needless calls of Golang fmt.Sprintf in 2019.
There’s a pattern.