svelte
                                
                                 svelte copied to clipboard
                                
                                    svelte copied to clipboard
                            
                            
                            
                        Support Native CSS Nesting
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
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 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.
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.
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
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
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.
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;
        }
    }
}
Can we support:
:global {
  .foo {
    /*...*/
  }
}
Like we do in preprocessors?
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.
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
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
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 @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
Is this closed by #10491 ?
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.
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 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
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
Nested CSS support was added to Svelte 5 (it's not practical to backport it to 4) so I'll close this