Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Native CSS Nesting #8587

Closed
Tracked by #9549
jrmoynihan opened this issue May 14, 2023 · 19 comments
Closed
Tracked by #9549

Support Native CSS Nesting #8587

jrmoynihan opened this issue May 14, 2023 · 19 comments

Comments

@jrmoynihan
Copy link

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

@Jothsa
Copy link

Jothsa commented May 16, 2023

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.

@karimfromjordan
Copy link

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.

@jrmoynihan
Copy link
Author

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.

@karimfromjordan
Copy link

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: csstree/csstree#186 (comment)

@karimfromjordan
Copy link

karimfromjordan commented May 21, 2023

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

@meduzen
Copy link

meduzen commented Jul 2, 2023

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.

@Jothsa
Copy link

Jothsa commented Jul 13, 2023

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

@AlbertMarashi
Copy link

Can we support:

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

Like we do in preprocessors?

@enyo
Copy link

enyo commented Nov 18, 2023

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.

@AlbertMarashi
Copy link

AlbertMarashi commented Nov 19, 2023

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
Copy link

AlbertMarashi commented Nov 20, 2023

I have created a PR that partially implements CSS nesting. Would like some people to help review & suggest improvements #9549

@enyo
Copy link

enyo commented Nov 20, 2023

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 :)

AlbertMarashi added a commit to AlbertMarashi/svelte that referenced this issue Nov 27, 2023
Excluding CSS type element selectors

closes sveltejs#9320
closes sveltejs#8587
AlbertMarashi added a commit to AlbertMarashi/svelte that referenced this issue Nov 27, 2023
@AlbertMarashi
Copy link

@enyo @jrmoynihan I've fully implemented support for CSS nesting in #9549, including support for type (element) selectors, combinator prefixes and & suffixes

@AlbertMarashi
Copy link

Is this closed by #10491 ?

@meduzen
Copy link

meduzen commented Feb 23, 2024

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.

@Dan503
Copy link

Dan503 commented Feb 25, 2024

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.

@Jothsa
Copy link

Jothsa commented Feb 25, 2024

@Dan503 It looks like that was included in the pull request #9549 (comment) which was merged in this pr #10491 (comment)

@AlbertMarashi
Copy link

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

@Rich-Harris
Copy link
Member

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants