Description
When a className template literal contains a class name built by directly concatenating a static hyphenated prefix with an interpolation (no space between them, e.g. `size-${size}`), sortTailwindcss splits the token at the hyphen and reattaches the interpolation to the following class instead of leaving it in place. This changes the string's runtime value, not just its formatting.
Note on dynamic Tailwind classes: yes, building class names via string concatenation (e.g. `bg-${color}`) goes against Tailwind's own guidance, since the static content scanner can't resolve it to generate CSS. That's a fair criticism of the source code in one of the examples below — but it's orthogonal to this bug. A formatter's contract is to never change a program's runtime behavior, no matter how the input is written. Even already-broken/non-functional input shouldn't be silently rewritten into differently broken output. The fix belongs in the sorter's tokenizer, not in "don't write it that way."
Repro
.oxfmtrc.json:
{
"sortTailwindcss": {
"functions": ["clsx", "cn", "cva"]
}
}
Input:
export const Foo = ({ size }) => {
return <div className={`rounded size-${size}`}>hi</div>;
};
Run: oxfmt --write Foo.jsx
Expected
Either the class list is left unsorted (since size-${size} can't be statically resolved), or size-${size} is treated as a single atomic token and reordered as a whole:
<div className={`size-${size} rounded`}>hi</div>
Actual
<div className={`size- rounded${size}`}>hi</div>
The formatter decided the token ends right before ${, split there, and glued the trailing ${size} onto the next class (rounded) instead of the one it was originally hyphenated to (size-). At runtime this evaluates to e.g. size- rounded36 instead of rounded size-36.
A second real-world case, where the interpolation sits mid-list with a trailing /10 opacity modifier:
Input:
<div className={`mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-${color}/10`} />
Output:
<div className={`bg- mb-4 flex h-12 w-12 items-center justify-center rounded-full${color}/10`} />
Same defect: bg- and an unrelated class (rounded-full) end up concatenated with the interpolation and the trailing /10.
Notes
- Reproduces regardless of whether the string is a bare
className={...} template literal or wrapped in a listed sort function.
- Suggested fix: when a class token contains an unresolvable interpolation directly adjacent to static text (no whitespace boundary), treat the whole token as opaque and either skip sorting for that string or move it as a single unit — never split at the interpolation boundary and reassign the tail to a neighboring token.
Environment
- oxfmt 0.58.0
- Config:
sortTailwindcss enabled with a stylesheet path and functions: ["clsx", "cn", "cva"]
Description
When a
classNametemplate literal contains a class name built by directly concatenating a static hyphenated prefix with an interpolation (no space between them, e.g.`size-${size}`),sortTailwindcsssplits the token at the hyphen and reattaches the interpolation to the following class instead of leaving it in place. This changes the string's runtime value, not just its formatting.Note on dynamic Tailwind classes: yes, building class names via string concatenation (e.g.
`bg-${color}`) goes against Tailwind's own guidance, since the static content scanner can't resolve it to generate CSS. That's a fair criticism of the source code in one of the examples below — but it's orthogonal to this bug. A formatter's contract is to never change a program's runtime behavior, no matter how the input is written. Even already-broken/non-functional input shouldn't be silently rewritten into differently broken output. The fix belongs in the sorter's tokenizer, not in "don't write it that way."Repro
.oxfmtrc.json:{ "sortTailwindcss": { "functions": ["clsx", "cn", "cva"] } }Input:
Run:
oxfmt --write Foo.jsxExpected
Either the class list is left unsorted (since
size-${size}can't be statically resolved), orsize-${size}is treated as a single atomic token and reordered as a whole:Actual
The formatter decided the token ends right before
${, split there, and glued the trailing${size}onto the next class (rounded) instead of the one it was originally hyphenated to (size-). At runtime this evaluates to e.g.size- rounded36instead ofrounded size-36.A second real-world case, where the interpolation sits mid-list with a trailing
/10opacity modifier:Input:
Output:
Same defect:
bg-and an unrelated class (rounded-full) end up concatenated with the interpolation and the trailing/10.Notes
className={...}template literal or wrapped in a listed sort function.Environment
sortTailwindcssenabled with a stylesheet path andfunctions: ["clsx", "cn", "cva"]