system-ui / theme-ui

Build consistent, themeable React apps based on constraint-based design principles

Home Page:https://theme-ui.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Provide an escape hatch for increasing the specificity of generated selectors

kapowaz opened this issue · comments

Is your feature request related to a problem? Please describe.

I’m currently migrating a legacy system written using react-bootstrap to Theme UI. As the process of migration is undertaken, components written in each approach must live alongside one another. This legacy system is full of Bootstrap styles for styling generic elements without the need for applying any classnames, but since the system also supports light and dark mode via a class on the html element, there are a number of selectors like this (which have a specificity of 0,1,1):

html.dark a {
  color: #f78b08;
}

Existing components which choose to override the default link style can do so by applying a set of classnames to that component, and since in all cases these selectors have at least one element name and one or more classnames, these selectors have a higher specificity than the generic link style; for example this selector which has a specificity of 0,2,1:

html.dark .list-group-item-action {
  color: #262626;
}

The problem emerges when you attempt to migrate the component to Theme UI by replacing these classnames with an object style, using the sx prop, since this will always apply just a single (generated) classname with a specificity of 0,1,0. What happens is this rule ends up having lower specificity than the base style, and so its properties are overridden:

html.dark a {
  color: #f78b08;
}

/*
This generated selector has lower specificity than the rule
above, and so the color property will be overridden.
*/
.css-15vgksq-SpectrumListGroupItem {
  color: #262626;
}

Describe the solution you'd like

I’d like the ability to increase the specificity of a generated selector, either by prefixing it with an element name, or (even more preferably) by having an optional generated selector prefix, which raises the specificity of the selector created by Theme UI. The API / syntax (borrowing a similar syntax used by Sass) might look something like this:

<a href="#foo" sx={{
  '.specialLink &': {
    color: '#262626',
  }
}}>Link Text</a>;

This would then generate something like this in the output CSS and HTML:

.css--agfhr9-specialLink.css-15vgksq-SpectrumListGroupItem {
  color: #262626;
}
<a href="#foo" classname="css--agfhr9-specialLink css-15vgksq-SpectrumListGroupItem">Link Text</a>

Importantly, the generated selector would have a specificity of 0,2,0, giving it a higher specificity.

An alternative would be to allow Theme UI to be configured to apply a global generated prefix to all generated selectors, potentially even using an ID. This could be configured to be applied to the chosen root element, and then all emitted selectors could be descendant selectors of that generated ID:

#css--agfhr9-myGlobalId .css-15vgksq-SpectrumListGroupItem {
  color: #262626;
}
<body id="css-agfhr9-myGlobalId">
  <a href="#foo" classname="css-15vgksq-SpectrumListGroupItem">Link Text</a>
  ...
</body>

Describe alternatives you've considered

So far, the only workaround I’ve found is to use the !important suffix, which feels like a hack and only allows a single additional level of specificity before the same problem will arise, or document source order comes into play.

Hey, thanks for the thoughtful issue! Reading through this, I don't think this is something we'd try to offer; if you're making a fresh app with Theme UI, specificity is very rarely an issue, & we advise against using global styles, though it totally makes sense using them in a migration like this. Especially with color modes, I think using CSS custom properties is a much easier solution, & you can use the ones automatically generated by Theme UI in your existing global CSS in order to reduce specificity there.

Does that help at all? Anything I can advise on?

Doesn't this work at this moment? AFAIK it's supported by Emotion, so we're getting it for "free".
@kapowaz

sx={{
  '&': {}, // should give you .className.className
  '&&': {} // should give you .className.className.className
}}

@lachlanjc

if you're making a fresh app with Theme UI, specificity is very rarely an issue, & we advise against using global styles, though it totally makes sense using them in a migration like this.

I appreciate this is unlikely to be an issue with new apps, but I think as Theme UI matures and is brought into existing projects, having a strategy for managing migration from older CSS methodologies is going to become increasingly important; I’d go so far as to say it could ultimately become an obstacle to adoption.

Especially with color modes, I think using CSS custom properties is a much easier solution, & you can use the ones automatically generated by Theme UI in your existing global CSS in order to reduce specificity there.

I think even using CSS custom properties you’re still going to need a mechanism for triggering whichever colour mode you’re currently in, right? So even if the actual value is dynamic based on a custom property, it’ll need either a classname or an @media rule to enable it; the technique I was looking at previously would be to do something like this, which would let me reduce the specificity to 0,0,1:

@media (prefers-color-scheme: dark) {
  :root {
    --theme-ui-colors-primary: #F78B08;
  }
}

a {
  color: var(--theme-ui-colors-primary);
}

Right now my team is carefully managing our migration by only updating specific components to Theme UI (which are published as individual packages, so they can be staged individually). The surface area of impact (and need for regression testing) to change link styles across the whole application would be sufficiently large I’d be reluctant to touch it for the moment, though.

This is a big part of why I talked about ‘escape hatches’; every abstraction leaks, and so having a (heavily-caveated) mechanism to avoid specificity problems with a CSS framework is probably important. But that lets me neatly segue to…

@hasparus

Doesn't this work at this moment? AFAIK it's supported by Emotion, so we're getting it for "free".

sx={{
  '&': {}, // should give you .className.className
  '&&': {} // should give you .className.className.className
}}

Fantastic, thank you! It appears I need to use two ampersands to get the double classname (single just emits the classname as per normal), but doing so I was able to drop the !important and get the desired effect. Can I suggest that this enter the documentation for Theme UI explicitly? I’m new to the framework (and I’ve no prior experience with Emotion) so the ways in which the two overlap aren’t obvious to me. I’d be happy to create a PR myself if you’d be interested?

As an interesting side-effect of using this double ampersand selector syntax, I’ve discovered that the toHaveStyleRule() matcher from @emotion/jest no longer correctly identifies the style properties being applied. stylis is able to parse them correctly, but there are two sets of properties: one for a single classname, with no style properties, and another with both, where all the style rules are applied; naturally the current behaviour is to use the former as the set of rules to check against, but it’s empty so the test fails.

I might create a separate issue for that though!