svelte icon indicating copy to clipboard operation
svelte copied to clipboard

Support Native CSS Nesting

Open jrmoynihan opened this issue 2 years ago • 7 comments

Describe the problem

Native CSS Nesting without a pre-processor is stable since Chromium 112 (March 2023), and is in technical preview of Safari 16.5.

However, this syntax is not supported by Svelte's processor in <style> tags. Especially in instances like using Svelte in third-party web containers where you might not have control over the availability of additional pre-processors, this makes writing the style rules far simpler/easier than using complex selectors or a multitude of classes, allowing more rapid and readable prototyping.

Surprisingly, there's no open or closed issue covering this topic.

Sources: https://caniuse.com/css-nesting https://web.dev/web-platform-04-2023/ https://developer.chrome.com/articles/css-nesting/ https://webkit.org/blog/13813/try-css-nesting-today-in-safari-technology-preview/

Describe the proposed solution

Treat nested selectors as valid syntax. Given this html:

<p>
  We really <span>should</span> support this
  <strong class="red">because it's part of the web platform...</strong>
</p>

All of these syntaxes should work without error:

<style>
  /* p strong */
  p {
    strong {
      line-height: 1.2;
    }
  }
  /* strong.red */
  strong {
    &.red {
       color: red;
    }
  }
  /* p > span */
  p {
    > span {
      color: orange;
    }
  }
</style>

Alternatives considered

Using a pre-processor is the alternative, but it adds unnecessary bloat to the DX.

Importance

nice to have

jrmoynihan avatar May 14 '23 10:05 jrmoynihan

Pretty sure you need

p {
  & strong {
    line-height: 1.2;
  }
}

The other is invalid. I think a good rule of thumb is every must start with a symbol.

Jothsa avatar May 16 '23 17:05 Jothsa

I think there is still an issue. According to this Chrome article the following should work but in Svelte it doesn't:

<script>
  let name = 'world';
</script>

<div class="card">
  <h1>Hello {name}!</h1>
</div>

<style>
  .card {
    :is(h1) {
      color: red;
    }
  }
</style>

It does however work with the additional ampersand &.

  .card {
    & :is(h1) {
      color: red;
    }
  }

So my guess is that it is currently intentionally always expecting the & to make it easier for the CSS parser to parse.

karimfromjordan avatar May 19 '23 12:05 karimfromjordan

I think there is still an issue. According to this Chrome article the following should work but in Svelte it doesn't:

<script>
  let name = 'world';
</script>

<div class="card">
  <h1>Hello {name}!</h1>
</div>

<style>
  .card {
    :is(h1) {
      color: red;
    }
  }
</style>

It does however work with the additional ampersand &.

  .card {
    & :is(h1) {
      color: red;
    }
  }

So my guess is that it is currently intentionally always expecting the & to make it easier for the CSS parser to parse.

Ya, I was wrong on the first example (it always requires either a starting symbol like an &, or a nested :is() pseudo-selector). But this example with the :is() selector should work without an ampersand.

jrmoynihan avatar May 19 '23 18:05 jrmoynihan

It doesn't work without an ampersand. I tried it in a REPL and in SvelteKit. From what I understand the CSS parser that Svelte uses doesn't support optional ampersands and potentially other features yet, see: https://github.com/csstree/csstree/discussions/186#discussioncomment-4578093

karimfromjordan avatar May 20 '23 11:05 karimfromjordan

I found another potential issue. The following styles:

<style>
  div {
    & :global(*) {
      color: yellow;
    }
  }
  div :global(*) {
    color: red;
  }
</style>

produce the following output:

<style>
  div.svelte-ofba00 {
    & :global(*) {
      color: yellow;
    }
  }
  div.svelte-ofba00 * {
    color: red;
  }
</style>

which means either :global() doesn't work with CSS nesting or there is another way to do this.

Edit: Actually, it looks like nested rules aren't scoped at all and are global by default https://svelte.dev/repl/783f0b40b80140efbcf486d29a9c41a4?version=3.59.1

karimfromjordan avatar May 21 '23 17:05 karimfromjordan

Hi, I would like to highlight a bug created due to the lack of support of native CSS nesting. See the REPL.

As code in nested selectors is not processed, and thus output as is, @keyframes names used in these selectors are not prefixed with the classname hash, leading to an animation that can’t be used in nested selectors.

Minimal input:

<style>

@keyframes dialog-background-light {
  to { background: oklch(1 0 0 / .3); }
}
	
dialog::backdrop {
  /* works as intended: animation is renamed with a hashed ID */
  animation: dialog-background-light .5s ease-out forwards;
}

dialog {
  &::backdrop {
    /* animation is not renamed */
    animation: dialog-background-light .5s ease-out forwards;
  }
}

</style>

Output (beautified):

@keyframes svelte-1ci0amd-dialog-background-light {
  to {
    background: oklch(1 0 0 / 0.3);
  }
}
dialog.svelte-1ci0amd::backdrop {
  animation: svelte-1ci0amd-dialog-background-light 0.5s ease-out forwards;
}
dialog.svelte-1ci0amd {
  &::backdrop {
    /* animation is not renamed */
    animation: dialog-background-light 0.5s ease-out forwards;
  }
}

As you can see, the non-nested dialog::backdrop receives the expected animation name (svelte-1ci0amd-dialog-background-light) while the nested one receives dialog-background-light.

meduzen avatar Jul 02 '23 20:07 meduzen

Nested media rules are not working correctly. These two examples should be equivalent but only the last one works.


a {
     &::after {
         @media (prefers-reduced-motion: reduce) {
             opacity: 0;
             transition: opacity var(--nav-transition-time) ease-in-out;
        }
    }
}


a {
    @media (prefers-reduced-motion: reduce) {
         &::after {
             opacity: 0;
             transition: opacity var(--nav-transition-time) ease-in-out;
        }
    }
}

Spec for reference

An example of nesting @media from the spec

Jothsa avatar Jul 13 '23 16:07 Jothsa

Can we support:

:global {
  .foo {
    /*...*/
  }
}

Like we do in preprocessors?

AlbertMarashi avatar Sep 25 '23 16:09 AlbertMarashi

Pretty sure you need

p {
  & strong {
    line-height: 1.2;
  }
}

The other is invalid. I think a good rule of thumb is every must start with a symbol.

I think that's not the case anymore.

enyo avatar Nov 18 '23 21:11 enyo

Pretty sure you need

p {
  & strong {
    line-height: 1.2;
  }
}

The other is invalid. I think a good rule of thumb is every must start with a symbol.

I don't think that's the case anymore.

That's correct however there are some complexities in supporting it and currently only the most recent version of some browsers supports it due to the complexities of distinguishing between a CSS declaration property and CSS rule

I am currently working on implementing CSS nesting for Svelte and will probably opt to require the & prefix for the first implementation

AlbertMarashi avatar Nov 19 '23 22:11 AlbertMarashi

I have created a PR that ~~partially~~ implements CSS nesting. Would like some people to help review & suggest improvements https://github.com/sveltejs/svelte/pull/9549

AlbertMarashi avatar Nov 20 '23 03:11 AlbertMarashi

That's correct however there are some complexities in supporting it and currently only the most recent version of some browsers supports it due to the complexities of distinguishing between a CSS declaration property and CSS rule

I'm sure it's more complex to parse, but now every browser, except Edge (which will have it soon since it's Chromium based) and Opera support nesting of element selectors without ampersand. So it would be really great if we could get support for this soon :)

enyo avatar Nov 20 '23 13:11 enyo

@enyo @jrmoynihan I've fully implemented support for CSS nesting in https://github.com/sveltejs/svelte/pull/9549, including support for type (element) selectors, combinator prefixes and & suffixes

AlbertMarashi avatar Nov 27 '23 04:11 AlbertMarashi

Is this closed by #10491 ?

AlbertMarashi avatar Feb 22 '24 03:02 AlbertMarashi

Is this closed by #10491 ?

It’s released in 5.0.0-next.57, but apparently the REPL (wanted to test my issue) is not compatible with v5 yet.

meduzen avatar Feb 23 '24 11:02 meduzen

I haven't seen any code samples posted above mention that & can also be placed at the end of a CSS rule.

.child {
  .parent & {
    color: red;
  }
}

Is equivalent to:

.parent .child {
  color: red;
}

Mentioning this since I saw comments say "the & always goes at the start" which isn't true. I want to make sure that the fix does support this use case.

Dan503 avatar Feb 25 '24 20:02 Dan503

@Dan503 It looks like that was included in the pull request https://github.com/sveltejs/svelte/pull/9549#issue-2001257949 which was merged in this pr https://github.com/sveltejs/svelte/pull/10491#issue-2137771558

Jothsa avatar Feb 25 '24 22:02 Jothsa

I haven't seen any code samples posted above mention that & can also be placed at the end of a CSS rule.

.child {
  .parent & {
    color: red;
  }
}

Is equivalent to:

.parent .child {
  color: red;
}

Mentioning this since I saw comments say "the & always goes at the start" which isn't true. I want to make sure that the fix does support this use case.

I'm not sure about the PR that @Rich-Harris implemented, but I assume he added that support too. My PR did have code to handle that

AlbertMarashi avatar Mar 03 '24 06:03 AlbertMarashi

Nested CSS support was added to Svelte 5 (it's not practical to backport it to 4) so I'll close this

Rich-Harris avatar Apr 03 '24 11:04 Rich-Harris