emotion
emotion copied to clipboard
Don't add classname for `:is()` and `:where()` selectors
The problem
I want to leverage :where selector (it is good for theming because it has 0 specificity) to style a component based on the parent attribute.
However, emotion prefix the style with the generated class name in front of :where which does not make the style works:
const Component = style('div')`
color: black
:where([data-color-scheme="dark"]) & {
color: white
}
`
turned into
<style>
// to make this style work, the `.css-9qpay1` at the beginning should be removed
.css-9qpay1:where([data-color-scheme="dark"]) .css-9qpay1{color:white;}
</style>
Here is the reproduction: https://codesandbox.io/s/emotion-forked-e9fyes?file=/index.js
Proposed solution
Should :is and :where be the exceptional case to not implicitly append to the class name?
const Component = style('div')`
:where([data-color-scheme="dark"]) & {
color: white
}
// result as ':where([data-color-scheme="dark"]) .css { color: white };
`
const Component = style('div')`
// explicitly declare the '&'
&:where([data-color-scheme="dark"]) {
color: white
}
// result as '.css:where([data-color-scheme="dark"]) .css { color: white };
`
Alternative solutions
Is there any workaround that I might have missed?
Additional context
Hi, I am from the MUI core team. We are working on the theming API to let developers theme components based on color schemes without having to read the mode from the theme.
Our API added an attribute to the html when the color scheme changes:
// light mode
<html data-color-scheme="light">
// dark mode
<html data-color-scheme="dark">
This is a byproduct of our compat plugin - however, this matches the behavior of Emotion 10 for all pseudo-selectors. I'd like to remove this compat plugin in the next major version of Emotion but I'm worried that changing the current behavior, even for a subset of pseudo-selectors, would be a breaking change that we can't do (we can continue discussing this though, those 2 are fairly new additions and it somewhat makes sense to treat them differently).
Right now you sort of can achieve this by combining namespace plugin and :global(): https://codesandbox.io/s/emotion-forked-rc5rpb?file=/index.js . Note that the generated rule has increased specificity because it uses the class name twice - maybe this could be fixed
@Andarist Thanks a lot for the info.
Note that the generated rule has increased specificity because it uses the class name twice - maybe this could be fixed
Can you point me to where to fix this? I can submit a PR.
Note, from what I check styled-components does not work as well.
My guess is that the problem is in this compat plugin:
https://github.com/emotion-js/emotion/blob/1554a7e264e05780b2c5bd74ccb20a92005ba61d/packages/cache/src/stylis-plugins.js#L88
But I didn't yet analyze this in full.
One alternative solution that I was thinking about (since adding the namespace plugin in MUI might be problematic in the long run) is to provide a way to "escape" a pseudo selector somehow so we could ignore it in the compat plugin (since it's the one that automatically prepends implicit & to all rules starting with :). Any ideas on what kind of a token we could use there?
I can think of:
~: it is the root dir in the terminal which could refer to global (this might not work, it refers to general sibling selectors)*: this looks like global (this might not work, it refers to the universal selector in CSS)!: or with the negate symbol to opt-out of implicit&
The usage would look like this, right?
const Component = style('div')`
color: black
~:where([data-color-scheme="dark"]) & {
color: white
}
`
I found a workaround! https://codesandbox.io/s/emotion-forked-6eco3g?file=/index.js
Note that this might be prone to some edge cases but it probably shouldn't matter for real-life cases in which you are gonna use this.
After thinking about this a little bit - I think that a comment annotation would be the easiest solution for this. Something like:
const Component = style('div')`
color: black
:where([data-color-scheme="dark"]) & {
/* emotion-no-compat */
color: white
}
`
I think I'm gonna add a dev-only warning for all instances of the "compat" behavior kicking in so people can start fixing their selectors in v11 but I can't change this default right now.
Note that whatever we could do about this in Emotion right now would require a new v11 minor version - and, from what I understand, you need this in MUI and you don't control which version of Emotion people have installed there as its a peer dep. So I'm unsure if any solution from our side would actually solve your issue - unless you expect people to update Emotion frequently.
I think that a comment annotation would be the easiest solution for this.
We have a util function that produces :where([data-color-scheme="dark"]) &. Does it mean that developers have to specify the comment all the time?:
styled('div')(({ theme }) => ({
// theme.getColorSchemeSelector('dark') => ':where([data-color-scheme="dark"]) &'
[theme.getColorSchemeSelector('dark')]: {
/* emotion-no-compat */
// ...styles
}
}))
It may not work for all use-cases, but in case anyone else comes looking, I've had success with using the * universal selector before the :where() pseudoclass along with the & class selector to apply the style to the desired class like so:
* :where() & {...
const texStyle = css`
color: red;
* :where([data-your-theme-value="example"]) & {
color: blue;
}
`
() => (
<>
<div data-your-theme-value='example'>
<span css={textStyle}> Blue Text Here </span>
</div>
<span css={textStyle}>Red Text Here</span>
</>
)
See the codesandbox PoC here
It may not work for all use-cases, but in case anyone else comes looking, I've had success with using the
*universal selector before the:where()pseudoclass along with the&class selector to apply the style to the desired class like so:
* :where() & {...const texStyle = css` color: red; * :where([data-your-theme-value="example"]) & { color: blue; } ` () => ( <> <div data-your-theme-value='example'> <span css={textStyle}> Blue Text Here </span> </div> <span css={textStyle}>Red Text Here</span> </> )See the codesandbox PoC here
Oh, great. Thanks for letting me know.
After thinking about this a little bit - I think that a comment annotation would be the easiest solution for this. Something like:
const Component = style('div')` color: black :where([data-color-scheme="dark"]) & { /* emotion-no-compat */ color: white } `I think I'm gonna add a dev-only warning for all instances of the "compat" behavior kicking in so people can start fixing their selectors in v11 but I can't change this default right now.
Unfortunately, this solution breaks down completely when using Emotion object notation.
<div
css={{
color: 'black',
':where([data-color-scheme="dark"]) &': {
/* emotion-no-compat */ // <== this is just a JS comment; will not be interpreted by Emotion.
color: 'white',
}
}}
>