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

Theming #1550

Closed
Rich-Harris opened this issue Jun 20, 2018 · 39 comments
Closed

Theming #1550

Rich-Harris opened this issue Jun 20, 2018 · 39 comments
Labels
awaiting submitter needs a reproduction, or clarification

Comments

@Rich-Harris
Copy link
Member

This question has come up enough times that we should probably come up with a solution — the question being 'how can I create a component kit (or similar) that supports theming?'.

One possible option would be to rely on CSS variables, but I think we can do better. The Svelteish way to tackle this would be to do it at compile-time. I propose something like this:

<!-- my-cool-component-kit/Widget.html -->
<div class="widget">
  <h1>{title}</h1>
</div>

<style>
  .widget {
    padding: $coolComponentKit.padding.normal;
  }

  h1 {
    color: $coolComponentKit.fgColor;
  }
</style>

The consumer of the component would then add something like this to their compiler options:

const { js } = svelte.compile(source, {
  variables: {
    coolComponentKit: {
      padding: {
        normal: '10px',
        larger: '20px'
      },
      fgColor: 'red'
    }
  }
});

Or, if the component library shipped a standard set of themes:

import * as themes from 'my-cool-component-kit/themes.js';

const { js } = svelte.compile(source, {
  variables: {
    coolComponentKit: themes.pastel
  }
});

Of course, we could do all this with preprocessors, which might be better as it doesn't lock us into a particular design:

import variables from 'svelte-preprocess-variables';
import * as themes from 'my-cool-component-kit/themes.js';

const { js } = svelte.compile(source, {
  preprocess: {
    style: variables({ coolComponentKit: themes.pastel })
  }
});

Now that I think about it, that might be preferable, if it was a 'blessed' approach that we could point people towards.

We might also consider supporting expressions:

<style>
  .foo {
    background-color: $(darken(coolComponentKit.colors.main, 0.5))
  }
</style>

Let's shave this yak! Any preferences for syntax? And should it be baked in or do we take the safer option of a preprocessor?

Also, as someone who doesn't really use component kits or CSS frameworks it's possible that I'm underestimating the problem — if you think this solution doesn't go far enough then please weigh in.

@lukeed
Copy link
Member

lukeed commented Jun 20, 2018

I'd like to see caps-locked variables everywhere for global-type theming. The full caps indicates (to me at least) that it is a global value, and that you didn't accidentally overlook coolComponentKit being defined elsewhere in the component.

Also, all-capped variables would work the same in stylesheets, regardless of pre/post-processing. Having the $ prefix within style tags (only) is one extra thing to remember & interferes/competes with SASS, SCSS, and Stylus syntax. (Stylus users aren't bound to $ for variables, but it's typically used as it shares w/ SASS.)

The preprocessor approach looks correct to me. And, if i understand correctly, basically would act as a giant rollup-plugin-replace on the whole application -- made especially easy if variable names are uniform.

@rob-balfre
Copy link
Contributor

The issue I faced using preprocessors is getting it playing nicely with svelte-loader / webpack. Any changes to the variables are ignored as svelte-loader has no reason to watch them so I ended up having to hack around with nodemon (https://github.com/rob-balfre/svelte-preprocess-vars). Maybe I'm just not proficient enough with webpack!

CSS variables might be enough for some but they don't let you access variables in your component's JS, which is a deal breaker for the projects I'm working on.

I'd really like to see it baked into Svelte with the a compiler variables option.

@Conduitry
Copy link
Member

Webpack does provide a way to declare an additional file system dependency, but I don't know that svelte-loader exposes that.

@rob-balfre
Copy link
Contributor

@Conduitry ah I missed that little nugget in the docs. Thanks!

@arxpoetica
Copy link
Member

I favor a preprocessor-- I'm already using one (PostCSS), and it's so powerful it's going to be hard to replace all the odds and ends such a thing can do-- it really is as versatile as one is willing to code it to be. Perhaps the correct thing to do is to just make a "blessed" or sanctioned approach to using one of the 4 main css preprocessors (Stylus, SCSS, Less, PostCSS).

@silentworks
Copy link

I am with the preprocessor option.

@kaisermann
Copy link
Member

kaisermann commented Jun 20, 2018

It would be really nice if we could overwrite a variable in a certain context as well (don't know if I'm thinking too far here):

<!-- Component.html -->
<div>
	<slot></slot>
</div>

<style>
	div {
		color: $textColor;
	}
</style>
<!-- App.html -->
<Component ref:component>
	Red colored text
</Component>

<style>
	ref:component {
		$textColor: 'red';
	}
</style>

This would help a lot with a bunch of props I have to support just for customizing a component's style (bgColor, textColor, etc)

@arxpoetica
Copy link
Member

It would be really nice if we could overwrite a variable in a certain context as well

Again, this can easily be done with preprocessors, like SASS, etc.

@Ryuno-Ki
Copy link

Can't contribute much to the way, how you want to process it, but I can point you to some articles explaining how to write maintainable CSS …

Maybe you can work towards those approaches …

@kaisermann
Copy link
Member

The second link from @Ryuno-Ki is kind of what I'm looking for.

Is it possible to do that kind of theming (even if each component style is processed individually and doesn't know about other components or global css variables)?

@stalkerg
Copy link
Contributor

Again, this can easily be done with preprocessors, like SASS, etc.

How? Looks like it more about components isolations and preprocessors can't help here.

@rob-balfre
Copy link
Contributor

I've created a PR that exposes webpack's loader dependencies api to svelte-loader. Should make theming with a preprocessor easier. https://github.com/sveltejs/svelte-loader/pull/66/files

@arxpoetica
Copy link
Member

arxpoetica commented Jun 21, 2018

How? Looks like it more about components isolations and preprocessors can't help here.

@stalkerg Not so, I do it with my own setup. I have a global variables.postcss file that gets imported into the compile section of each svelte component style tag, and cascades down to each component that way. If I need to change a var for an individual component, it's easy enough.

There's definitely something magic Sapper could do with preprocessors along this line. The question is just how far to take it. Should inheritance cascade down from parent to child in all cases? That could be interesting.

@arxpoetica
Copy link
Member

For what it's worth, here's my pared down Sapper webpack config settings:

const sharedConfig = require('./shared-config')

module.exports = {
	...
	module: {
		rules: [
			{
				test: /\.(html|sv|svelte|svg)$/,
				use: {
					loader: 'svelte-loader',
					options: {
						...
						preprocess: {
							style: sharedConfig.style,
						},
					},
				},
			},
		]
	},
	...
}

and the import

const postcss = require('postcss')
const plugins = require('../server/build/postcss.config.vars').plugins

module.exports.style = ({ content }) => {

	return new Promise((fulfil, reject) => {
		postcss(plugins)
			.process('@import \'routes-includes\';\n' + content, {
				from: 'src',
				syntax: require('postcss-scss'),
				map: true,
			})
			.then(result => {
				if (result.css && typeof result.css === 'string') {
					fulfil({
						code: result.css.toString(),
					})
				} else {
					fulfil({ code: '', map: '' })
				}
			})
			.catch(err => reject(err))
	})
}

Note, I didn't share the plugins file, but it's just an array of PostCSS plugins/settings.

I think my setup might be a little buggy with maps, but it's working for me sufficiently.

@stalkerg
Copy link
Contributor

@arxpoetica your approach working if you control all components only for a case with the component from npm it will be a problem.

@NikolayMakhonin
Copy link

This is one of the solutions of the svelte theming problem. I would like to see something similar in svelte, with supported components from npm:
https://github.com/NikolayMakhonin/svelte-themes-preprocess

@o-t-w
Copy link

o-t-w commented May 23, 2019

One possible option would be to rely on CSS variables, but I think we can do better.

CSS variables are awesome and a standard. I don't see how doing things with JS is better.

@philholden
Copy link

This is my first Svelte app: MVP theming using CSS vars and a JS theme

  • Colors can be overridden on a single instance via props
  • Colors can be assigned from JS theme store
  • Updating theme colors in the store causes colors to update live in app

https://codesandbox.io/s/angry-kapitsa-86li9

@philholden
Copy link

I need to test performance for this as some say 2018 performance for CSS vars is poor if they are at the top level. For leaf nodes it is fine.

https://blog.jiayihu.net/css-custom-properties-performance-in-2018/

The problem is that CSS vars are inherited by all descendants so the cost of setting a CSS var may be proportional to number of descendants for container elements.

I did an experiment using shadow dom but even if the var is set in shadow dom it still propagates to the children contained in .

@philholden
Copy link

I am hoping that if I only use vars declared on the element then the browser is able to work it does not need to recalc everything.

@o-t-w
Copy link

o-t-w commented May 30, 2019

Perhaps the correct thing to do is to just make a "blessed" or sanctioned approach to using one of the 4 main css preprocessors (Stylus, SCSS, Less, PostCSS).

CSS preprocessors are going to fall out of favour. The only thing holding back CSS is Internet Explorer 11, which is on its death be. Things you can do with native CSS variables that you can not do with preprocessor variables:

  • see them and change them in dev tools
  • change them with javascript
  • change them with media queries

Other features of CSS preprocessors are slowly being brought into the browser as native CSS features. They are fundamentally better and more powerful than Sass/Less/PostCSS/Stylus variables.

@mikemaccana
Copy link

mikemaccana commented Jun 10, 2019

Other features of CSS preprocessors are slowly being brought into the browser as native CSS features.

I think that will happen too, but indeed slowly. I can and do use CSS variables for theming now, but there's no CSS mixin standard now and based on past experiences there probably won't be a CSS mixin for 7 years. There's no standard way to share styling between components right now without creating visual junk classes (.shiny .big etc) which bloat the resulting HTML. If your login box is shiny, you should be able to change that by editing the styles for .login-box not modifying some HTML to remove a class.

On that basis I think it'd be good to use a preprocessor (not JS) to keep styling in stylesheets, until the preprocessor is no longer needed.

So yes, I agree with the other commentors about a 'blessed' way to use a given preprocessor.

@slominskir
Copy link

slominskir commented Jun 26, 2019

Constructable Stylesheets should probably be considered too as they appear to be on their way: https://developers.google.com/web/updates/2019/02/constructable-stylesheets

Looks like the advantage of this approach is styles are really shared so if you have a ton of the same component there is only one style object that is parsed and interpreted. Saves memory and parsing time. There are so many ways to share style in custom elements that it's hard to keep track of. Here is a probably incomplete list:

  1. Global stylesheets can be used if you don't use a shadow dom
  2. CSS Vars can be used, but they don't scale well if you have a lot of CSS rules you want to be configurable
  3. Include an external stylesheet from within a custom element using link or import. This might cause duplicate downloading and parsing (Chrome might be smart about this: Sharing styles across custom elements WICG/webcomponents#282 (comment)), prevents style from being combined and minified with the rest of the application, users witness a flash of unstyled content, and path to file is problematic
  4. A framework can be used to essentially copy and paste style directly inside your custom component. If you have a lot of components then there is a lot of wasted memory and parsing.
  5. Constructable Stylesheets allow defining a reusable stylesheet, but it is bleeding edge so it might not be supported in your browser yet
  6. Shadow ::part and ::theme (https://meowni.ca/posts/part-theme-explainer/). Looks like ::part is available in Chrome, but I can't find a ton of info on this approach (https://www.chromestatus.com/feature/5763933658939392).

Deprecated methods

@maddeye
Copy link

maddeye commented Jul 18, 2019

You want it the Svelte way?
Why focusing on something like preprocessing?

Maby we can try something like this:

<!-- themedComponent/widget.html -->
<theme>
  padding {
    normal: 5px;
    xl: 20px;
  }

  color {
    blue: #0000ff;
  }
</theme>

<style>
  div {
    padding: $theme.padding.normal;
  }

  h1 {
    color: $theme.color.blue
  }
</style>

<div> <!-- padding: 5px -->
  <h1>{title}</h1> <!-- color: #0000ff -->
  <SubWidget />
</div>
<!-- themedComponent/subwidget.html -->
<theme>
  color {
    blue: #00ffff;
  }
</theme>

<style>
  div {
    padding: $theme.padding.xl;
  }

  h1 {
    color: $theme.color.blue;
  }
</style>

<div><!-- padding: 20px -->
  <h1>{subtitle}</h1> <!-- color: #00ffff -->
</div>

So basiclly you can define a part of the theme in each component, but also can overwrite it locally. If you don't overwrite it locally it sets the value based on the highest order component. With this we don't need any prepocessors like sass.

@Conduitry Conduitry added the awaiting submitter needs a reproduction, or clarification label Sep 9, 2019
@YoungElPaso
Copy link

YoungElPaso commented Nov 26, 2019

Sass modules could be a part of this. Forwarded styles then imported by components?

My use case is that I want to develop a set of Svelte components while simultaneously developing a CSS framework I can hand off/use with non-Svelte projects.

IE the styles for the framework would be derived from the Svelte Components, with resulting CSS scoped per component, but also with some global variables/theme options. I imagine this as another compile time option to export CSS as a single file or per-component.

I'll admit, this is tangential, but I think some of the concerns: namely having global style variables to import overlap.

That said, foisting this work into JS is an attractive option that probably just works at the moment.

TLDR: A related issue (the one I'm attempting) to deal with is encapsulated in these questions:
How do I write CSS that is re-usable, but also can be scoped? Can I do this with SFC's? Can I get the authoring wins of Svelte but be able to re-use (some) code in non-Svelte projects? Or is that too tangential and asking too much of one tool?

@pkrogel-kws
Copy link

I like Maddeye’s approach. Theming with variables also available for use in JS is the only thing stopping me from switching from React to Svelte. Any timeline for when this is coming?

@stalkerg
Copy link
Contributor

stalkerg commented Dec 9, 2019

but also can overwrite it locally

@maddeye should be possible overwrite theme variables outside and it should be possible to provide default values. Otherwise, you have to somehow import the default theme from a package/module.

@maddeye
Copy link

maddeye commented Dec 9, 2019

True. I'm with you here. Also there should be default values like colors or something like Medium/Large from Bootstrap.

@mallsoft
Copy link

//css
div :global(.hmm){
...
}

//dom
<div><Component/>...

// Component
<someelem class="hmm">

a pattern i often end up using, it's not a beautiful pattern but it works.

I would love to be able to skip the wrapping div and maybe just ...

//css
Compontent{ ... }

//dom
<Component/>

@YoungElPaso
Copy link

@maddeye I like the idea of the theme, but the problem I see with that issue is that nesting i.e. cascading becomes really important and maybe unpredictable. And doesn't this syntax just propose a different pre-processor in effect? (That said, this isn't really a criticism, because this is all compiled anyway).

I do like the idea of the approach though, and it does resemble the Sass @use/@forward module implementation, which I, as a Sass guy would prefer to use. My use case is different though in that I want to author CSS via Sass that is useful in Svelte components, as well as vanilla HTML etc. But I agree, if there were a 'Svelte way' that would be awesome.

@maddeye
Copy link

maddeye commented Dec 17, 2019

@practicalRope But this is just another workaround, not a real solution.

@YoungElPaso I understand what you mean. Maby it would be possible to compile my approache to Sass and add the Sass Compile to the build pipeline. With this everyone could use my idea, aka the 'svelte way', or simply add there own sass files. Also this would be probably easier to maintain.

@YoungElPaso
Copy link

@maddeye I'm using Sass modules myself. If it goes poorly I'll mention it here. Obviously that fits my use case and your mileage may vary. I do like the developer experience your idea would promote - in my case maintaining separate Sass files is a necessary evil, but I'd love to ditch it.

@zahachtah
Copy link

Is there a consensus among the svelte devs yet on the best approach for theming? Am in a situation where I need to choose the route I am going and want to try to walk the main path :-)

@swyxio
Copy link
Contributor

swyxio commented Feb 2, 2020

in case it helps anyone, i stick my theme in a store, then put it into css variables https://github.com/sw-yx/swyxdotio/blob/master/src/routes/_layout.svelte#L13-L28 on my site https://www.swyx.io/writing/ (up top where it says "change theme" right now)

@maddeye
Copy link

maddeye commented Feb 3, 2020

As far as i can see this we came together that we want to use my approach with an additional Sass support. The thing is i don't think I'm good enough to implement it myself.

@sw-yx This looks like a nice workaround. I think i will use this myself until we implement our solution ;).

@swyxio
Copy link
Contributor

swyxio commented Feb 3, 2020

yeah. if you need lighten/darken you can implement in js. https://css-tricks.com/snippets/javascript/lighten-darken-color/

@DominikGuzei
Copy link

Looking forward to a native svelte solution for theming 🎉 in the meanwhile i have created https://github.com/CodeAdventure/svelte-jss which integrates JSS with svelte & sapper and supports cross-component theming with SSR support.

Of course it's not perfect since it adds a (small, ~6kb) js overhead to the runtime. But due to SSR integration css is embedded in the static html and is immediately rendered 💪

@papertokyo
Copy link

This won't be an option for everyone, but Tailwind has a robust theming setup built-in. https://tailwindcss.com/docs/theme/

@antony
Copy link
Member

antony commented Apr 9, 2020

discussion continues here: sveltejs/rfcs#13

@sveltejs sveltejs locked as resolved and limited conversation to collaborators Apr 9, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
awaiting submitter needs a reproduction, or clarification
Projects
None yet
Development

No branches or pull requests