Simplifying class name strings
How to simplify conditional class name strings.
Simplify class name strings without external libraries.
Using a class based approach for css, such as Tailwind can often mean long strings of class names. I created
this simple function to concatenate and deal with conditional renderings. Instead of writing strings such as ${x ? "y" : "z"}
you can supply a list of strings or an array beginning with a boolean expression which can deal with conditionals.
// class-names.ts
function _clean(cn: string) {
// replace multi spaces globally and ignore new lines
return cn.replace(/(\s+|\r\n|\n|\r)/gm, " ").trim()
}
type CSSClass =
| string
| CSSClass[]
| [boolean, CSSClass]
| [boolean, CSSClass, CSSClass]
| undefined
function _cn(arg: CSSClass, classes: string[]) {
if (!arg) {
return
}
if (Array.isArray(arg)) {
if (arg.length > 0) {
if (typeof arg[0] === "boolean") {
switch (arg.length) {
case 1:
// nonsense so ignore
break
case 2:
// only render if true
if (arg[0]) {
_cn(arg[1], classes)
}
break
default:
// conditionally render
arg[0] ? _cn(arg[1], classes) : _cn(arg[2], classes)
break
}
} else {
arg.forEach(arg => _cn(<CSSClass>arg, classes))
}
}
} else {
const argType = typeof arg
if (argType === "string" || argType === "number") {
classes.push(arg)
}
}
}
/**
* Concatenates strings of class names together to form a class name string.
* Useful for breaking up long tailwind class strings.
* Also adds conditional rendering. [condition, 'classes'] will only add the
* classes if condition is true. [condition, 'classes1', 'classes2'] adds
* classes1 or classes2 conditionally. Also supports recursive conditionals.
* [condition1, [condition2, 'classes1', 'classes2'], 'classes3'].
*
* @param args string or array of strings of classnames. Also supports condition c
* @returns a space separated string of class names.
*/
export function cn(...args: CSSClass[]): string {
const classes: string[] = []
_cn(args, classes)
// join all the pieces into one then split on space
// and remove duplicates
return _clean(classes.join(" "))
}
/**
* Concatenates strings of class names together to form a class name string.
* Useful for breaking up long tailwind class strings. This variant keeps
* only unique names in the order they appear.
* Also adds conditional rendering. [condition, 'classes'] will only add the
* classes if condition is true. [condition, 'classes1', 'classes2'] adds
* classes1 or classes2 conditionally. Also supports recursive conditionals.
* [condition1, [condition2, 'classes1', 'classes2'], 'classes3'].
*
* @param args string or array of strings of classnames. Also supports condition c
* @returns a space separated string of class names.
*/
export function cnu(...args: CSSClass[]): string {
const used = new Set<string>()
const classes: string[] = []
_cn(args, classes)
// join all the pieces into one then split on space
// and remove duplicates
return _clean(
classes
.filter(c => {
// keep track of tokens already seen
const ret = !used.has(c) && c !== "" && c !== " "
used.add(c)
return ret
})
.join(" ")
)
}
export cn
In the example the arrays containing 3 elements where the first element is a boolean. such as [show2, "p4", "p5"] will conditionally add class p4 or p5 depending on whether show2 is true. You can nest these expressions to create more complex one line conditional expressions that do not require cumbersome string interpolations, for example [show, [show2, "p4", "p5"], "p6"], if show is true, then add class p4 or p5 depending on whether show2 is true.
// component.tsx
import cn from "../lib/class-names"
...
const show = true
const show2 = true
const show3 = true
return(<div className={cn("p1 p2", "p3", [show, [show2, "p4", "p5"], "p6"], [show3, "p7"])}>)