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

Optionally scope imported CSS files to component level #7125

Closed
rodoch opened this issue Jan 12, 2022 · 17 comments
Closed

Optionally scope imported CSS files to component level #7125

rodoch opened this issue Jan 12, 2022 · 17 comments

Comments

@rodoch
Copy link

rodoch commented Jan 12, 2022

Describe the problem

Currently, when stylesheets are imported within either a script or style block within a Svelte component their styles are applied globally.

<style>
    @import 'carbon-components-svelte/css/g10.css';
</style>

This is a sensible default behaviour. Some design system libraries etc. need to touch things like the body and html elements.

However, I think this can also be problematic and a limitation in some cases. For example, if you are producing a component library you often want to make sure that all of your styles are encapsulated and do not wreak havoc with the parent webpage/app styling.

Describe the proposed solution

A solution for this currently exists in Vue, where you can do something like:

<style scoped src="@/assets/css/main.css"></style>

...and it wil scope the contents of the CSS file. This is very ergonomic.

I want to be clear, the extent of this request is in relation to imported external CSS files. In researching this issue, I came across some rather in-depth debates as regards providing a scoped/unscoped modifier for the style block. I am not trying to rehash this debate here and I'm not wedded to this particular approach, I'm just using it as an example of an equivalent functionality in another popular framework.

Futhermore, given that Svelte does not wrap components in a generic div in the generated HTML I can see reasons why this may be difficult to implement. But given the potential benefits for users I thought I should ask the question at least.

Alternatives considered

  1. Don't implement it: In this case, I think the current behaviour could at least be better documented. Given that most people associate Svelte with effective style encapsulation, this behaviour with imported CSS can come as a surprise to people, certainly it took me a minute to work it out.
  2. Perhaps this can be solved in userland with a preprocessor? I tried to see if I could get this to work with PostCSS but it's proved beyond my skills so far.

Importance

would make my life easier

@Conduitry
Copy link
Member

This would have to be some sort of preprocessor, because the core compiler just takes a single string as input and doesn't look at the filesystem at all.

@Tropix126
Copy link

svelte-preprocess already adds support for an src attribute on <style>. It also adds support for a global attribute which wraps all of the style's selectors in :global().

@Tropix126
Copy link

You can also use a postcss plugin to import local files (or sass's @use syntax if you're doing that). Those will both insert styles into the style tag before svelte gets ahold of it's contents, meaning they will be scoped as well.

@rodoch
Copy link
Author

rodoch commented Jan 13, 2022

You can also use a postcss plugin to import local files (or sass's @use syntax if you're doing that). Those will both insert styles into the style tag before svelte gets ahold of it's contents, meaning they will be scoped as well.

Thanks @Tropix126. I had tried using sass - and the @use syntax prior to filing the issue. My experience was that the styles remained unscoped.

Also, can I ask in relation to your first comment, what would be the advantage of adding the global modifier if I want to keep all of the styles encapsulated and not leaking out into the parent page/component?

@Tropix126
Copy link

Thanks @Tropix126. I had tried using sass - and the @use syntax prior to filing the issue. My experience was that the styles remained unscoped.

In which way are you including your styles?

Also, can I ask in relation to your first comment, what would be the advantage of adding the global modifier if I want to keep all of the styles encapsulated and not leaking out into the parent page/component?

This was in response to your mentioning of a scoped/unscoped attribute.

@rodoch
Copy link
Author

rodoch commented Jan 13, 2022

In which way are you including your styles?

When using svelte-preprocess + sass:

<style lang="scss">
    @use 'carbon-components-svelte/css/g10.css';
</style>

Of course, besides using Sass I also tried:

<style>
    @import 'carbon-components-svelte/css/g10.css';
</style>

and

<script context="module">
    import 'carbon-components-svelte/css/g10.css';
</script>

In all cases the imported styles are applied globally.

@eni9889
Copy link

eni9889 commented Jan 29, 2022

@rodoch have you found any solution to this? I’m looking to do the same thing. I would like bootstrap to be scoped when imported

@rodoch
Copy link
Author

rodoch commented Jan 29, 2022

I don't believe it's possible within the framework or with existing tools at this time. I've raised this issue as a feature request

@eni9889
Copy link

eni9889 commented Jan 30, 2022

@rodoch I saw this plugin but haven't been able to make it work yet https://github.com/micantoine/svelte-preprocess-cssmodules it's got to be possible through some kind of preprocessing

@jangxyz
Copy link

jangxyz commented Apr 8, 2022

I made it work by using svelte-preprocess with less and wrapping a custom div element (scope) around it. I think this is what Tropix meant. There is no scope feature, but I can add a specific classname in my case.

Something like this:

<div class="default">
  <pre><code class="language-javascript">console.log("Hello world!")</code></pre>	
</div>

<div class="github">
  <pre><code class="language-javascript">console.log("Hello world!")</code></pre>	
</div>

<div class="solarized-dark">
  <pre><code class="language-javascript">console.log("Hello world!")</code></pre>	
</div>

<style lang="less">
  .default :global {
    @import (less) 'https://highlightjs.org/static/demo/styles/default.css';
  }
  .github :global {
    @import (less) 'https://highlightjs.org/static/demo/styles/base16/github.css';
  }
  .solarized-dark :global {
    @import (less) 'https://highlightjs.org/static/demo/styles/base16/solarized-dark.css';
  }
  .atelier-seaside-light :global {
    @import (less) 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.6/styles/atelier-seaside.light.min.css';
  }
</style>

@SarcevicAntonio
Copy link
Member

SarcevicAntonio commented Nov 15, 2022

needed feature to make reusing styles across different component less of a hassle.

EDIT: apparently svelte-preprocess makes this a thing: https://github.com/sveltejs/svelte-preprocess#external-files

<style src="./style.css"></style>

Everything from style.css gets applied to the component with scoped selectors.
Works like a charm.

@rodoch
Copy link
Author

rodoch commented Nov 22, 2022

This is a great solution - many thanks! I think that closes this issue.

@null-dev
Copy link

null-dev commented Apr 30, 2023

Unfortunately, you cannot use multiple <style> tags so you cannot import multiple CSS files into a single component.

For those who want to do that, I've written a rudimentary CSS preprocessor:

import MagicString, {Bundle} from "magic-string";
import * as path from "path";
import * as fs from 'node:fs/promises';

/* Make `@import "./whatever.css" scoped;` statements import CSS into the component's CSS scope */
function importCSSPreprocess() {
  async function importCSS({ content, filename }) {
    function matchAllImports(str) {
      const globalRegex = /@import\s+(".*"|'.*')\s+scoped\s*;/g;
      const matches = [];
      let match;
      while ((match = globalRegex.exec(str)) !== null) {
        const start = match.index;
        const end = start + match[0].length;
        matches.push({ start, end, file: match[1].substring(1, match[1].length - 1) });
      }
      return matches;
    }

    const imports = matchAllImports(content);
    if(imports.length > 0) {
      let lastStart = null;
      const state = new MagicString(content, { filename });
      const remove = (start, end) => state.clone().remove(start, end);
      let out = [];
      const deps = [];
      for(const { start, end, file } of imports.reverse()) {
        // Right
        if(lastStart != null) {
          out.push(remove(lastStart, content.length).remove(0, end));
        } else {
          out.push(remove(0, end));
        }
        const absPath = path.join(path.dirname(filename), file);
        deps.push(absPath);
        const text = (await fs.readFile(absPath)).toString();
        out.push(new MagicString(text, { filename: absPath }));
        lastStart = start;
      }
      // Left
      const first = remove(lastStart, content.length);
      const bundle = new Bundle();
      bundle.addSource(first);
      for(let i = out.length - 1; i >= 0; i--) {
        bundle.addSource(out[i]);
      }

      return {
        code: bundle.toString(),
        map: bundle.generateMap(),
        dependencies: deps
      };
    } else {
      return {code: content};
    }
  }
  return { style: importCSS }
}

You can add it to your svelte.config.js, then add it to the preprocessor list:

export default {
  preprocess: [
    importCSSPreprocess(), // <--
    svelteAutoPreprocess(),
  ],
};

Now you can use @import "./whatever.css" scoped;.

For example, the following CSS:

<style>
@import "./a.css" scoped;
@import "./b.css" scoped;

.another-style { display: block }
</style>

will get converted into:

<style>
contents of a.css will be here
contents of b.css will be here

.another-style { display: block }
</style>

@wrightwriter
Copy link

@null-dev thank you so much! it's worth publishing as a package.

After some headscratching, got it running in webpack + svelte-preprocess:

			{
				test: /\.svelte$/,
				use: {
					loader: 'svelte-loader',
					options: {
						// stuff
						preprocess: [
							// @ts-ignore
							importCSSPreprocess(),
							SveltePreprocess({
								sass: true,
								scss: true
							}),
						],
					},
				},
			},

@ryoppippi
Copy link

For those who want to do that, I've written a rudimentary CSS preprocessor:

Nice preprocessor !

coz this is useful, I uploaded it to jsr as a package!

https://jsr.io/@ryoppippi/svelte-preprocess-import-css

@hyunbinseo
Copy link
Contributor

  • @sveltejs/vite-plugin-svelte used in the SvelteKit default template does not support external files
  • svelte-preprocess should be installed for the <style src="./style.css"></style> syntax (answered above)

However, svelte-preprocess does provide extra functionalities not available with Vite preprocessors, such as template tag, external files, and global styles (though it's recommended to use import instead). If those features are required, you can still use svelte-preprocess, but make sure to turn off it's script and style preprocessing options.

  • @sveltejs/kit@2.5.28 has vite-plugin-svelte as peer dependency, and cannot be uninstalled.
  • Configuring SvelteKit as preprocess: sveltePreprocess() still seems to utilize vite-plugin-svelte

Console:

Internal server error: src/routes/(tailwind)/ebook/+page.svelte:276:2 Invalid selector
Plugin: vite-plugin-svelte
File: src/routes/(tailwind)/ebook/+page.svelte:276:2

HMR error overlay:

[plugin:vite-plugin-svelte] src/routes/(tailwind)/ebook/+page.svelte:276:2 Invalid selector
src/routes/(tailwind)/ebook/+page.svelte:276:2

Would there be a workaround for this? Would love to use scoped global styles.

@ryoppippi
Copy link

FYI We can use css modules out of the box with rsbuild

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

10 participants