sveltejs / svelte

Cybernetically enhanced web apps
https://svelte.dev
MIT License
76.98k stars 4k forks source link

Theming #1550

Closed Rich-Harris closed 4 years ago

Rich-Harris commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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

rob-balfre commented 5 years ago

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

arxpoetica commented 5 years ago

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 commented 5 years ago

I am with the preprocessor option.

kaisermann commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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

NikolayMakhonin commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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

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

philholden commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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:

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 commented 4 years ago

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 commented 4 years ago

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: https://github.com/w3c/webcomponents/issues/282#issuecomment-122186756), 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 commented 4 years ago

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.

YoungElPaso commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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

mallsoft commented 4 years ago
//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 commented 4 years ago

@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 commented 4 years ago

@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 commented 4 years ago

@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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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

DominikGuzei commented 4 years ago

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 commented 4 years ago

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

antony commented 4 years ago

discussion continues here: https://github.com/sveltejs/rfcs/pull/13