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

Preserve unused CSS when nested within used rules #10844

Open
LeaVerou opened this issue Mar 19, 2024 · 2 comments
Open

Preserve unused CSS when nested within used rules #10844

LeaVerou opened this issue Mar 19, 2024 · 2 comments

Comments

@LeaVerou
Copy link

LeaVerou commented Mar 19, 2024

Background

This is an idea about how to solve the very common author pain point where CSS that is not actually unused gets removed.
Here is a sampling of closed issues on this:

In many of these the suggestion is to work around it via :global(), but of course that does not preserve the scoping that in many of these cases is desirable.
I think a lot of what the discourse is missing is that this could easily happen when integrating existing libraries into Svelte.
For example, D3 Svelte is the current state of the art for high fidelity interactive visualizations in the dataviz community,
but D3 does not actually know about Svelte. Thankfully, it largely works well with Svelte out of the box, except when it doesn’t.
Case in point, the use case that prompted this: I was generating a chart via D3 and wanted to style ticks via CSS. My HTML looked like this:

<g class="gridlines" transform="translate({margin.left}, 0)" bind:this={yAxisGridlines} />

and the JS that populated the gridlines was:

let yAxisGridlines;

$: {
	d3.select(yAxisGridlines).call(d3.axisLeft(yScale));
}

Then, I tried to do this in my CSS:

.gridlines line {
	stroke-opacity: .5;
}

Nope, no result. It took some time to realize it was being commented out as "unused".

I even tried a nested version (I'm using Svelte 5):

.gridlines {
	line {
		stroke-opacity: .5;
	}
}

Nope squared this time 😁

Proposed solution: Take advantage of CSS nesting

The reasoning given in many of these cases is that Svelte cannot apply classes to elements it doesn't know about. Fair.

But what about descendants? E.g. in my case, the .gridlines container was in my CSS, and line was within it. line did not actually need any classes for scoping, because it's already scoped via its parent.

Instead of parsing selectors to figure out which ones can work like this and which ones cannot (which is a can of worms I do not wish on anyone), I propose a much easier solution: we take advantage of nesting. If the parent of a nested set of rules is not unused, then the children are automatically also not unused (essentially wrapped in :global()). This way Svelte 5 can provide a workaround for this longstanding issue in an intuitive way, that involves relatively low implementation complexity.

@Rich-Harris
Copy link
Member

Hey! I don't think we want to treat nesting as semantically different than the equivalent non-nested selectors — unused CSS removal is useful, and expecting people to understand that Svelte interprets one form differently from the other is too big an ask. It would be especially confusing when people refactor between them, and the behaviour changes under their feet.

Instead, I think the solution here is to make it easier to author global styles within a component. There's a proposal in #10815 that uses nesting syntax to achieve this — the styles above would become this:

-.gridlines {
 .gridlines :global {
  line {
    stroke-opacity: .5;
  }
}

Or, more generally:

.stuff-in-my-template :global {
  .stuff-d3-cares-about {
    /* ... */
  }

  .other-stuff-d3-cares-about {
    /* ... */
  }

  .etc {
    /* ... */
  }
}

@kwangure
Copy link
Contributor

kwangure commented Apr 21, 2024

I don't recall where I previously saw this issue and a potential solution discussed. What if we had a :local selector? This selector tells Svelte, "I know you don't see the element, but it'll exist somehow". When used, styles are preserved and the CSS class to preserve scoping is added.

INPUT:

/* GOOD: no error */
:local(.seemingly-non-existent) {
    color: red;
}
/* GOOD: error */
.actually-non-existent {
    color: blue;
}

OUTPUT:

.seemingly-non-existent.s-r@nd0m {
    color: red;
}

:local could solve an additional separate issue too. I've always had issue with the fact that Svelte bails validating your styles if they're dynamic:

<script>
    /** @type {'dark' | 'light} */
    let theme = 'dark';
</script>
<div class="red {theme}">foo</div>
<style>
    .red.light { color: red }
    .red.dark { color: maroon }
    /* BAD: no error because Svelte doesn't know what `theme` contains */
    .actually-non-existent {
        color: blue;
    }
</style>

Now that we're breaking things with v5, what if we validated all styles and didn't bail on dynamic styles given that a :local selector exists as an escape hatch.

/* GOOD: no error */
:local(red.light) { color: red }
:local(.red.dark) { color: maroon }
/* GOOD: error because it's not in markup and not :local */
.actually-non-existent {
    color: blue;
}

With the recent rise of headless libraries, Svelte bails too often on style validation to be useful.

This could be filed as a separate issue, but if we go the :local route it makes sense to track it here. Two birds with one issue.

EDIT:
Oops. 🙈 I see it's the issues that are linked above that mentioned a similar solution.

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

3 participants