withastro / roadmap

Ideas, suggestions, and formal RFC proposals for the Astro project.
292 stars 29 forks source link

CSS Ordering #540

Closed bluwy closed 1 year ago

bluwy commented 1 year ago

Body

Summary

Allow styles in Astro files to override global styles.

Background & Motivation

A common pattern in an application is to have some global styles in a global style file like so:

global.css

input {
  border-width: 1px;
}

And then to use them in an Astro component.

---
import '../global.css';
---
<style>
  input {
    border-width: 2px;
  }
</style>

<input type="text">

The intent is to override the styles on this page (or component). But due to chunking of shared dependencies, we cannot guarantee that CSS ordering will be correct.

And due to the use of :where() for scoping, scoped styles can't win in specificity to apply the overriden styles. (as :where preserves specifity, unlike Vue or Svelte that adds attributes/classes to increase specificity) (See original RFC for details)

We need a solution for this.

Goals

Non-Goals

Example

Below are possible solutions:

  1. Go back to using scoped-attributes/classes (like Vue and Svelte), this would increase specificity by 1.
  2. Use @layer: https://developer.mozilla.org/en-US/docs/Web/CSS/@layer. However, this doesn't have better browser support than :where.
  3. Manually mark certain CSS files to be global (via astro.config?), and order them first to have lower priority.
  4. [FUTURE] Use @scope: https://www.w3.org/TR/css-scoping-1/
matthewp commented 1 year ago

@bluwy can up with the idea for this configuration value:

scopedStyleStrategy: 'where' | 'class' | 'layer'

Note, we don't have 'layer' now and don't intend to add it, just a demonstration of a possible future approach.

natemoo-re commented 1 year ago

I still think :where is the technically correct option, but that doesn't matter if it doesn't match with user expectations. But I do want to caution that this proposes changing our scoping behavior as a solution to what I see as a bundling problem—maybe that bundling problem is an inherent issue with trying to bundle CSS and not something we can solve. Changing our scoping behavior will make the problem less noticeable, but CSS ordering will still be hard to reason about. I think there might be other options here that we have yet to explore.

For additional context, one of the motivating factors for the original decision to switch to :where was that the following is not true:

Go back to using scoped-attributes/classes (like Vue and Svelte), this would increase specificity by 1.

Specificity is actually increased by N where N equals the number of clauses in your selector. For a simple selector like div, the output of div.astro-HASH has a specificity increase of one. But for a complex selector like div ul li, the output of div.astro-HASH ul.astro-HASH li.astro-HASH has a specificity increase of three.

This compounding behavior is what we had hoped to avoid by switching to :where. The original behavior was technically deterministic, but invisibly more specific depending on the structure of your selector. With :where, at least the specificity of the selector you author is the specificity of the final selector.

bluwy commented 1 year ago

Interesting! The N specificity case seems to apply to Svelte, but checking Vue it instead generates div ul li[data-v-472cff63]. I wonder what the tradeoffs are, but this could be something we need to decide beforehand.

EDIT: The tradeoff might be that Svelte leans on :global(), and Vue leans on >>> to opt-out/in of things.

lilnasy commented 1 year ago

I would like to avoid N specificity, but at the same, when I use descendant selectors, the last selector is usually global so that it can style slotted elements.

matthewp commented 1 year ago

Merged.