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

Javascript Interpolation in Styles via CSS Variables? #758

Closed
colah opened this issue Aug 8, 2017 · 26 comments
Closed

Javascript Interpolation in Styles via CSS Variables? #758

colah opened this issue Aug 8, 2017 · 26 comments

Comments

@colah
Copy link

colah commented Aug 8, 2017

Back in November, there was some discussion of whether svelte could support javascript variable interpolation in style sheets (sveltejs/v2.svelte.dev#9). Unfortunately, this didn't seem feasible, since CSS classes are per class, and can't be specialized per instance.

It seems like this could be possible using CSS variables. Consider adding the following (functional) svelte code to the standard counter example:

<div class="bar" style="--count:{{count}}"></div>

<style>
  .bar {
    width: calc(40px * var(--count) );
  }
</style>

This sort of pattern might be attractive for a a few reasons:

  • It avoids messy inline styling.
  • It allows pure CSS for certain kinds of interactivity (eg. :hover).
  • It may (?) be faster in certain situations like spritemap manipulation where one wants to prevent certain parts of the style from being updated? (I don't understand the underlying model well enough to know if or when this would be true).

(To be clear, I've only done a few toy things this way. I'm completely open to the possibility that it may not be a good idea at all!)

While the example above works, and might be handy, it seems unnecessarily clunky. It also runs a risk that CSS variables may end up with different names than their JS counterparts, accumulating technical debt.

Potential Svelte Integration

If svelte wanted to facilitate this kind of pattern, there's a broad spectrum of options it could take.

  1. At the very mild end, it could support a kind of CSS variable binding syntax. This could cut down on repretition and encourage variable names to stay in sync:
<div class="bar" css_bind:count ></div>
  1. The next step up might be to detect variable usage and implicitly bind them. So you'd just do this without explicitly setting up --count:
width: calc(40px * var(--count) );
  1. Yet stronger might be to allow javascript variables through the usual syntax and implicitly create the css variable:
width: calc(40px * {{count}} );

Or maybe this, if you wanted to still remind the user that it's a variable:

width: calc(40px * var({{count}}) );
  1. Finally, one could go all the way in:
width:  {{40 * count}}px;

Which would compile into something like:

<div class="bar" style="--svelte-css-1234:{{40 * count}}px"></div>

<style>
  .bar {
    width: var(--svelte-css-1234);
  }
</style>

Potential Downsides

  • This could lead to less transparent CSS code when inspected in the browser. Compare:
width: 120px;

--count: 3;
width: calc(40px * var(--count) );

--svelte-css-1234: 120px;
width: var(--svelte-css-1234);

On the flip side, it may make some things more transparent -- especially if we can keep variable names synced to javascript.

  • It may also complicate the model for understanding what your code is doing behind the scenes...
@Conduitry
Copy link
Member

I do really like this idea, but I'm worried about having to put disclaimers next to certain features about 'if you use this feature, the compiled code will not work in Internet Explorer'. Unless something's changed, there aren't official browsers that Svelte commits to supporting, but in the past we've aimed to produce code that runs in the last few versions of IE.

@Rich-Harris
Copy link
Member

That REPL example is super cool! I actually think that's already quite nice and usable. May even start using it in a few places where I don't need to support IE.

glam manages to polyfill CSS vars somehow. Haven't studied the implementation, but maybe there's something in there that Svelte could be doing. (No idea.)

If we did want a more concise approach, I think it's number 4:

width:  {{40 * count}}px;

The implementation certainly could get a bit hairy, especially if polyfilling was involved, but I don't see a huge amount of value in the intermediate versions over and above the example that works today.

@colah
Copy link
Author

colah commented Aug 8, 2017

@Conduitry: that's a very reasonable reservation to have.

According to this blog post, Microsoft now supports CSS custom properties in their "Windows Insider Release". And the blog post is by the program manager for Edge and has a very positive tone, so I'd read that as a pretty strong signal that they intend to support it. No idea how long it takes features to move from their insider release to public consumption, let alone to trickle out to wide spread use.

@Rich-Harris
Copy link
Member

One other consideration — csstree won't parse this:

div {
  color: {{color}};
}

Just to make things a bit more annoying!

@Conduitry
Copy link
Member

It might be neat to revisit this, now that we're no longer supporting IE11. It also looks like recent versions of csstree are able to parse CSS that contains mustaches/tags.

@colah
Copy link
Author

colah commented Aug 5, 2018

For what it's worth, this would still be very exciting to me!

My svelte code constantly turns into tags with style="multiple lines of css styling with mustaches" that's not very readable. It really feels like that this would make it much cleaner!

@saabi
Copy link

saabi commented Mar 2, 2019

Here's a REPL with the Counter example in v3.

It's not too cumbersome, but some support for doing it directly in CSS, while keeping compatibility with other preprocessors would be great.

Something like this:

<input type=number bind:value={count}>
<button on:click={() => count += 1}>count</button>
<div class="count-bar"></div>

<!--
var-prefix could be optional, default would just be '--' which 
would replace all CSS vars that match a component variable.
However, specifying it improves readability as it makes 
intent explicit.
And perhaps it should be required to enable replacement, so
as to avoid surprises with unexpected replaced vars.
-->
<style var-prefix='--s-'> 
	.count-bar {
		height: 40px;
		width: calc( var(--s-count) * 1px + 1px);
		background-color: gray;
	}
</style>

@michalczaplinski
Copy link

@Rich-Harris Is this still a desirable feature? As per @colah's solution number 4:

width:  {{40 * count}}px;

I'd like to send a PR because I'm missing this coming from styled-components.

Also, could you clarify your comment there #1687 (comment) :

(To clarify: no, you can't use component state in <style>, because the styles are shared between all component instances. It would be very inefficient for the browser to have a copy of the styles per component; for coarse-grained customisation like the primary button, classes are the preferred mechanism, and for fine-grained per-component customisation, inline styles are the appropriate tool for the job.)

Perhaps I misunderstood, but does this mean that the compiler would have to create a copy of the styles for each component instance if we interpolate JS in <style>...? Does this prevent the current issue from being implementable?

PS. Just started using Svelte a couple days ago and, wow, I'm super impressed. Great work! 🙂

@pngwn
Copy link
Member

pngwn commented May 5, 2019

With the current CSS implementation, yes. There is one CSS file for each component, not for each component instance. In the above example you could have several instances of a component with several width values but only one CSS file.

The only way to make this work would be for the compiler to check which values were dynamic and manage those values in the component instance rather than in the CSS file. There is more to think about here as well (can you interpolate which nth-of-type gets a style applied to it?) How much of a CSS runtime would this introduce, and how would this conflict with svelte's general goal.

CSS is definitely something that we want to improve, so I don't mean to be negative but there are some important things to consider before implementing these kinds of changes both from a design point of view and a technical point of view.

@michalczaplinski
Copy link

@pngwn Thanks for the explanation - that makes sense. I ll have a look at how other libs deal with it and try to come up with some ideas.

@salvoravida
Copy link

salvoravida commented May 13, 2019

<script>
	import { css } from 'emotion';

	export let width = 200;

    $: boxStyle = css`
        border: solid 1px red;
        height: 200px;
    	width: ${width} px;
    	font-size: 1em;
    	white-space: nowrap;
    `;
</script>

<style>
	h1 {
		color: red;
	}
</style>

<div class={boxStyle}>
   <h1>Hello {width}!</h1>
</div>

styled component like in Svelte!

@Rich-Harris first: great works!

Some tips to make Svelte a real-big-apps alternative to React :

  1. Styled Components like, with runtime css is something too important in big Apps, so please try to do this with less boilerplate than my example.

  2. make possible to export multiple components from one file (like styled components)

  3. compiler should understand derivate invalidation props without $: statement
    if a variable is used inside the template than should be auto-invalidate even if without $:
    Think like syncronization beetween "scope state" and rendered html

<script>
	import { css } from 'emotion';
	export let width = 200;
        let boxStyle = css`
        border: solid 1px red;
        height: 200px;
    	width: ${width} px;
    	font-size: 1em;
    	white-space: nowrap;
    `;
</script>
<div class={boxStyle}>
   <h1>Hello {width}!</h1>
</div>

again: great works! :D

@salvoravida
Copy link

salvoravida commented May 14, 2019

Some POC i'm testing with emotion as css runtime :

App.svelte

<script>
    import { MyDiv, MyButton } from './styles';
	let width = 200;
	let toggleButton = true;
	let toggleDiv = true;
</script>

<input type="number" bind:value={width}>

<MyDiv width={width}  on:click={()=> { toggleDiv=!toggleDiv}}  >
Toggle Me
</MyDiv>

<MyButton width={width-40} primary={toggleButton} on:click={()=>  {toggleButton=!toggleButton }}/>

<MyDiv width={width}>
   Button toggle result : {toggleButton}
   Div toggle result : {toggleDiv}
</MyDiv>

styles.js

import { styled } from "svelte-styled-components";

export const MyDiv = styled.div`
  border: solid 1px red;
  height: 200px;
  width: ${props => props.width}px;
  flex-direction: column;
  display: flex;
  cursor:pointer       	
`;

export const MyButton = styled.button`
  margin-top:20px;
  background-color: ${props => (props.primary ? 'green' : 'blue')};
  height: 25px; 
  width: ${props => props.width || 80}px;
`;

https://codesandbox.io/s/l3n6w3lwom

feedback?

@constgen
Copy link

constgen commented May 19, 2019

What if make CSS react on variable changes like this?

<script>
let myVariable = '#000'; // mentioned in CSS
let anotherVariable; // not mentioned in CSS

// The next should be generated by the compiler to sync variable with CSS in reactive way
let css = componentElement.style; // don't know how to get this
$: {
  css.setProperty('--myVariable', myVariable);
}
</script>
<style>
div {
  color: var(--myVariable);
}
</style>

<div>Component content</div>

@seantimm
Copy link

I think this represents functionally what I'm hoping we could accomplish:
https://svelte.dev/repl/b4f7137e6df842218c3bd335ccb07be8?version=3.4.1

With it generated from:

<script>
let color = 'pink';	
</script>

<style>
h1 {
  background-color: var(--color);
}
</style>

<h1>Hello world!</h1>

I'm starting to slog my way through the code to see if I can start figuring out how to make this happen, but I'm new here. 😄

@seantimm
Copy link

I'd expect custom properties to be scoped by default just like everything else. Then we can generate compile errors if a variable is referenced that doesn't exist. We'd want to provide a way to reference globals... something like this?:

 background-color: :global(var(--color))

@kohlmannj
Copy link

kohlmannj commented May 27, 2019

Maybe there’s a path forward through Constructable Stylesheets? https://developers.google.com/web/updates/2019/02/constructable-stylesheets

@alexdilley
Copy link

alexdilley commented Jun 21, 2019

You can achieve CSS/JS interoperability with PostCSS; see here for an example with Sapper.

...for the flip-side:

<script>
import { customProperties as style } from '../styles/variables';

console.log(style['--theme']);
// #=> '#ff3e00'
</script>

@kaisermann
Copy link
Member

kaisermann commented Aug 15, 2019

I just released a tiny package that tries to solve this problem: https://github.com/kaisermann/svelte-css-vars. It uses an action to automatically do those style.setProperty @seantimm commented above.

REPL: https://svelte.dev/repl/1522fe3bdf904843a01101d9f900241d?version=3.8.1

@dhonx
Copy link

dhonx commented Aug 16, 2019

@kaisermann Great job.... when i saw @seantimm comment... i have an idea... but you already make it... very good

@ryanditjia
Copy link

Hi everyone, came from React/Gatsby world and building my next client site with Svelte/Sapper. This might be Sapper-specific but it seems the proper thread to ask.

I have a global style:

:root {
  --color-accent: rebeccapurple;
}

Nav.svelte (child of _layout) uses this variable to set text color:

<style>
  nav {
    color: var(--color-accent);
  }
</style>

<nav>
  ...
</nav>

Now, this is a client site and they carry different brands, thus different routes should be able to override this accent color:

routes/brand/[slug].svelte

<script context="module">
  export async function preload({ params }) {
    const res = this.fetch(fetchAccordingToParam)
    const data = await res.json()

    if (res.status === 200) {
      return {
	accentColor: data.accentColor,
      }
    } else {
      this.error(res.status, data.message)
    }
  }
</script>

<style>
  :global(:root) {
    --color-accent: accentColor_override_from_preload; // <--
  }
</style>

How can I achieve the above? I’ve tried using stores, but that causes the color to flicker (the override isn’t server-rendered, I prefer it done SSR).

As last resort, I could make Nav a child of route, but first I would love to hear if anyone has a solution.

Another angle to this question is: can data from route’s preload function be made available to _layout?

Thanks.

@alexdilley
Copy link

@ryanditjia I don't think this is possible in an SSR context (see #917).

You may be able, in your root layout's preload, to interpolate the current path (extracting out the brand) and fetch style-related data to then either instantiate a store (it's just store reactivity that's not honoured server-side) or simply use it to set css props at a root-level element (e.g. <main style="--color-accent: {accentColor}">).

@antony
Copy link
Member

antony commented Apr 11, 2020

Moving discussion to sveltejs/rfcs#13

@antony antony closed this as completed Apr 11, 2020
@jimmywarting
Copy link
Contributor

We also have this CSS.registerProperty method:

<script>
  window.CSS.registerProperty({
    name: '--my-color',
    syntax: '<color>',
    inherits: false,
    initialValue: '#c0ffee'
  })
</script>

<style>
h1 {
  background-color: var(--my-color);
}
</style>

<h1>Hello world!</h1>

only works in Blink doe... maybe there is a polyfill for that

@nemre
Copy link

nemre commented Apr 6, 2021

Please add support;
`<script>
export let variable = “#EFEFEF”
</script>

<style> main { color: {variable} } </style>`

@bluwy
Copy link
Member

bluwy commented May 24, 2021

I've made sveltejs/rfcs#51 to bring this discussion back. I don't think sveltejs/rfcs#13 really answered this issue as it only highlights passing CSS variables from parent to children, in contrast, this issue discusses about passing CSS variables within a component.

Glad to take any feedback.

@aradalvand
Copy link

aradalvand commented Dec 13, 2021

Setting CSS custom properties on elements via actions has one major downside which is that it won't work with SSR.
I should this issue should be reopened.

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

No branches or pull requests