Sort Tailwind classes on a JSX element or a CVA object
When to group
Inline className: Only group/sort when there are 8+ Tailwind class
names on a single element. For 1-7 classes, leave them as a plain string
(className="...") — do not wrap in cn(...) just for formatting.
Extracted constants / reusable arrays: When classes are pulled into a
named variable (e.g. const navItemBaseClasses = [...]) or a cva base,
grouping with comments is appropriate at any length because the variable
exists specifically to document a shared style contract.
cn is not a formatting tool. Only use cn(...) when you need
conditional/dynamic class merging, or when an element genuinely has 8+
static classes that benefit from grouping. A short static className string
is always preferred over a cn(...) call with no conditions.
Group labels
Group labels are allowed when they materially improve readability. As a rule of thumb:
- •1 group: no labels.
- •2-3 groups: labels are allowed if they clarify intent.
- •4+ groups: labels are recommended.
Remember: only label groups if there are more than 3 groups.
Here's an example in CVA (components/ui/button: buttonVariants).
From:
cva(
"ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{}
)
To:
cva(
[
// layout
"inline-flex items-center justify-center gap-2",
// shape
"rounded-md",
// typography
"text-sm font-medium whitespace-nowrap",
// focus
"ring-offset-background focus-visible:ring-ring",
// disabled
"disabled:pointer-events-none disabled:opacity-50",
// svg
"[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
// transitions
"transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
],
{}
)
Here's an example in JSX (components/ui/tabs: TabsTrigger).
From:
/* */
<div
className={cn(
"ring-offset-background focus-visible:ring-ring data-[state=active]:bg-background data-[state=active]:text-foreground inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm",
className
)}
/>
To:
<div
className={cn(
// layout
"inline-flex items-center justify-center",
// shape
"rounded-sm px-3 py-1.5",
// typography
"text-sm font-medium whitespace-nowrap",
// focus
"ring-offset-background",
"focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
// disabled
"disabled:pointer-events-none disabled:opacity-50",
// transitions
"transition-all",
// states
"data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className
)}
/>
Please note that:
- •
cnis a util that merges duplicate Tailwind classes, and handles aggregating a string of classes from all conditionals. Uses clsx and tailwind-merge. - •
cxis calls-variance-authority's exported version ofclsx. Use this instead ofcnbecausecnwill remove text-* classes but we need those unduplicated because they can both style colour and font-size!import { VariantProps, cva, cx } from "class-variance-authority"; - •
cvafrom class-variance-authority allows us to create type-safe UI components with variants. See: https://cva.style/docs