Escaping underscores in JavaScript contexts can cause mismatched class selectors
See original GitHub issueWhat version of Tailwind CSS are you using?
v3.1.6
What build tool (or framework if it abstracts the build tool) are you using?
Create React App v5.0.1
What version of Node.js are you using?
v16.15.0
What browser are you using?
Observed in Firefox 103 and Chrome 103
What operating system are you using?
macOS 12.4
Reproduction URL
https://github.com/nbrombal/tailwind-escape-conflict
Describe your issue
Tailwind’s method of escaping underscores within arbitrary values conflicts with JavaScript when Tailwind classes are applied in a JavaScript context. For example, when using Tailwind classes in JSX as part of a ternary expression, or as a function argument for the popular classnames library. This can lead to the HTML that is ultimately rendered to the page not matching the CSS that Tailwind generates.
My specific situation is one where I want to use underscores in the content attribute of a pseudo-selector. In the following examples, expand_more
is the name of an icon in the Material Symbols icon font, and the font-symbols
utility applies that icon font so its ligature feature will translate the name into the icon.
I’m not the first person to have run into this (that would be @FelixZY in this discussion), but I don’t believe the cause of this edge case has been articulated until now.
Plain String, single escaped (working as intended)
The JSX source contains a simple string for the className prop:
<span className='before:font-symbols before:content-["expand\_more"]' />
✓ The generated HTML class is as expected.
<span class="before:font-symbols before:content-["expand\_more"]"></span>
✓ The generated CSS rule is as expected.
.before\:content-\[\"expand\\_more\"\]::before {
--tw-content: "expand_more";
content: var(--tw-content);
}
JavaScript String, single escaped
The JSX source uses a string in a JavaScript context (note that this runs afoul of my ESLint/Prettier config, YMMV):
/* eslint-disable no-useless-escape, prettier/prettier */
<span className={'before:font-symbols before:content-["expand\_more"]'} />
✗ Because of the JS context, the slash has been removed from the HTML class.
<span class="before:font-symbols before:content-["expand_more"]"></span>
✓ The generated CSS rule is as expected, but of course it now doesn’t match our slash-less HTML class.
.before\:content-\[\"expand\\_more\"\]::before {
--tw-content: "expand_more";
content: var(--tw-content);
}
JavaScript string, double escaped
The JSX source uses a string in a JavaScript context:
<span className={'before:font-symbols before:content-["expand\\_more"]'} />
✓ The generated HTML class is now as expected since we escaped our escape character.
<span class="before:font-symbols before:content-["expand\_more"]"></span>
✗ However, the generated CSS rule now incorporates the extra slashes, so it doesn’t match the HTML class.
.before\:content-\[\"expand\\\\_more\"\]::before {
--tw-content: "expand\_more";
content: var(--tw-content);
}
This could probably be resolved by having Tailwind use a different method to escape underscores that doesn’t match the way JavaScript does it. I’ve struggled to come up with an alternative to suggest because any option risks breaking existing content strings, but for simplicity’s sake I lean towards suggesting a single underscore to represent a space (which is the current behavior), and a double underscore to represent an underscore.
Issue Analytics
- State:
- Created a year ago
- Reactions:1
- Comments:12 (4 by maintainers)
Top GitHub Comments
@nbrombal Added a quick note here: https://github.com/tailwindlabs/tailwindcss.com/commit/3ef481af5242ca86777d4a1e1ff5630a5bb8efdb
Glad to hear the
String.raw
solution is sufficient, going to close this one then with that as the recommendation 👍One solution in JSX is to use
String.raw
when you need to escape an underscore in a class, for example:This works as expected, because JS won’t process the escape and preserves the
\_
in the string.The more I think about this the more I’m becoming convinced there’s not really an amazing answer here and we probably just need to live with the current behavior and accept that there’s some edge-cases you need to work around sometimes.
Going to leave this open for a couple more days in case anyone wants to collect some more examples here of situations where they have run into this so we can provide recommendations on how we would handle those situations and so that this issue can serve as a useful reference for anyone who runs into this in the future, but unfortunately don’t think we will end up acting on this unless we hear about it a lot more from a wider group in the community.