question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Escaping underscores in JavaScript contexts can cause mismatched class selectors

See original GitHub issue

What 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-[&quot;expand\_more&quot;]"></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-[&quot;expand_more&quot;]"></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-[&quot;expand\_more&quot;]"></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:closed
  • Created a year ago
  • Reactions:1
  • Comments:12 (4 by maintainers)

github_iconTop GitHub Comments

1reaction
adamwathancommented, Sep 2, 2022

@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 👍

1reaction
adamwathancommented, Aug 8, 2022

One solution in JSX is to use String.raw when you need to escape an underscore in a class, for example:

<span className={String.raw`before:font-symbols before:content-["expand\_more"]`} />

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.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Why are dashes preferred for CSS selectors / HTML attributes?
Perhaps a key reason why the HTML/CSS community aligned itself with dashes instead of underscores is due to historical deficiencies ...
Read more >
Underscore.js _.some Function - GeeksforGeeks
Context : This parameter contains the text which need to display. Return values: The return value which is either true (when at least...
Read more >
Battling BEM CSS: 10 Common Problems And How To Avoid ...
I believe the double-underscore pattern should appear only once in a selector name. BEM stands for Block__Element–Modifier ...
Read more >
CSS.escape() - Web APIs - MDN Web Docs
The CSS.escape() static method returns a string containing the escaped string passed as parameter, mostly for use as part of a CSS selector....
Read more >
4 Syntax and basic data types
In CSS, identifiers (including element names, classes, and IDs in selectors) can contain only the characters [a-zA-Z0-9] and ISO 10646 characters U+00A0 and ......
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found