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

SFC style CSS variable injection (new edition) #231

Merged
merged 20 commits into from Jul 16, 2021
Merged

Conversation

yyx990803
Copy link
Member

@yyx990803 yyx990803 commented Nov 16, 2020

This is an improved alternative of the previous version (<style vars> as proposed in #226)

Thanks to feedback by @Demivan and @privatenumber, #226 has a few notable issues that can be improved:

  1. Requires manual vars declaration for exposing variables that can be used.
  2. No obvious visual hint that a variable is injected and reactive
  3. Different behavior in scoped/non-scoped mode.
  4. In non-scoped mode, the CSS variables would leak into child components.
  5. In scoped mode, the global: prefix is required to use normal CSS variables declared outside of the component. It would be preferable that normal CSS variable usage stays the same in/out of the component.

This proposal addresses all of the above with the following usage:

<template>
  <div class="text">hello</div>
</template>

<script>
  export default {
    data() {
      return {
        color: 'red',
        font: {
          size: '2em'
        }
      }
    }
</script>

<style>
  .text {
    color: v-bind(color);

    /* expressions (wrap in quotes) */
    font-size: v-bind('font.size');
  }
</style>

Summary:

  • No need to explicit declare what properties are injected as CSS variables (inferred from usage of v-bind() in CSS)
  • Reactive variables are more visibly different
  • Same behavior in scoped/non-scoped mode
  • No leakage into child components
  • Usage of plain CSS variables are unaffected

Rendered

@yyx990803 yyx990803 added the sfc Single File Components label Nov 16, 2020
@CyberAP
Copy link
Contributor

CyberAP commented Nov 16, 2020

What do you think about writing those bindings without -- prefix, so we can better distinguish them from the native CSS Custom Properties?

<script>
export default {
  data() {
    return { baz: 'red' }
  }
}
</script>

<style>
.foo {
  color: var(:bar); /* binding */
  font-size: var(--global-font-size); /* native property */
}
</style>

We don't reference those transformed bindings in the code so they don't really require -- prefix.

Also, to avoid Prettier issues the --: prefix could be replaced with a # (also works for CSS parsers), though it might be confusing since url(#foo) can reference SVG path.

<style>
.foo {
  color: var(#bar); /* won't be affected by Prettier */
}
</style>

@yyx990803
Copy link
Member Author

@CyberAP I think it's better to keep the leading -- because it's a smaller spec mismatch than without it:

  1. --:foo can still be considered a custom property identifier that contains an invalid char
  2. :foo is not a custom property identifier (which is defined as an identifier with leading --) - in fact, CSS identifier tokens cannot start with :.

I think (2) has a higher risk of tooling incompatibility as it's more likely for a parser to fail to tokenize :foo as an identifier due to the invalid leading char.

@privatenumber
Copy link

  • Could there be an obfuscate/mangle/minify CSS vars option to enable on production? I'm interested in the following benefits:

    • Minification The CSS distribution size can be reduced if hashed var names are mangled, especially for long & semantic variable names. eg. if var(--:theme.color.secondary) can be compiled to var(--6b537420) instead of var(--6b53742-theme_color_secondary).

    • Obfuscation Like JS minification, mangled CSS property names can deter reverse engineers. I can't remember which CSS-in-JS library Instagram uses, but this is really nice:

      Screen Shot 2020-11-16 at 6 50 25 PM
  • Wondering if there's a scalability issue. If there's a page that renders 100 different themeable components that all reference an injected theme property, I'm thinking that would create 100 different CSS vars that all map to the same state. If all components that read from the same state can share one CSS variable, the performance can improve dramatically. Have a few ideas to address this but would like to validate whether this is a real concern first.

// Button.vue - example of a themable component reading from an injected state
<template>
	<button class="button">
		<slot />
	</button>
</template>

<script>
export default {
	inject: ['theme'],
};
</script>

<style>
.button {
	background-color: var(--:theme.color.primary);
}
</style>
// Input.vue - another example of a themable component reading from an injected state
<template>
	<input
        class="input"
        type="text"
    >
</template>

<script>
export default {
	inject: ['theme'],
};
</script>

<style>
.input {
	color: var(--:theme.color.primary);
}
</style>
// Layout.vue - injects theme state, and also has UI that rapidly updates theme properties
<template>
	<div class="layout">
		<label>
			Primary color:

			<!-- Every change here will update a different CSS var for each component that reads from this  -->
			<input
				type="color"
				v-model="theme.color.primary"
			>
		</label>

		<slot />
	</div>
</template>

<script>
export default {
	data() {
		return {
			theme: {
				color: {
					primary: 'red',
				},
			},
		};
	},

	provide() {
		return {
			theme: this.theme,
		};
	},
};
</script>

@xstxhjh
Copy link

xstxhjh commented Nov 17, 2020

font-size: var(--:font.size);
: .
vscode problems: ) expectedcss(css-rparentexpected)

Invalid CSS Var

:root {
  --font.size: 22px;
}

.text {
  font-size: var(--font.size);
}

valid CSS Var

:root {
  --fontSize: 22px;
}

.text {
  font-size: var(--fontSize);
}

will go wrong?

.text {
    color: var(--v-bind:color);

    /* shorthand + nested property access */
    font-size: var(--:font.size);
}

@ghost
Copy link

ghost commented Nov 17, 2020

What's the reason of having both long and shorthand notation? Just pick one...

@Kocal
Copy link

Kocal commented Nov 17, 2020

@web2033 We have v-bind:attr and :attr in templates, so it makes sense

@CyberAP
Copy link
Contributor

CyberAP commented Nov 17, 2020

From my personal experience v-bind is never used in projects in a long notation and having two types of notation has never been great because people have to choose one when framework should actually solve that problem for them.

@ghost
Copy link

ghost commented Nov 17, 2020

How about

.text {
    color: var(--v-bind-color);
    font-size: var(--v-bind-font-size);
}

since a finger is already on - anyways.
Valid CSS. No tooling tweaking needed.

@ghost
Copy link

ghost commented Nov 17, 2020

... or "vue-branded" vars 😁

<template>
  <div class="text">hello</div>
</template>

<script>
  export default {
    data() {
      return {
        color: 'red',
        font: {
          size: '2em'
        }
      }
    }
</script>

<style>
.text {
    color: var(--vue-color);
    font-size: var(--vue-font-size);
}
</style>

@yyx990803
Copy link
Member Author

yyx990803 commented Nov 17, 2020

@privatenumber the theme injection is an example, and yes it can potentially lead to inefficiency when used everywhere. Maybe for global themes it is still better to provide it as plain CSS variables at root and let it cascade:

<!-- App.vue -->
<script>
const theme = ref({
  color: 'red',
  // ...
})
</script>

<template>
  <div class="theme-provider>
    <input type="color" v-model="theme.color">
  </div>
</template>

<style>
.theme-provider {
  --color-primary: var(--:theme.color);
}
</style>

@edisdev
Copy link

edisdev commented Jul 1, 2021

@m4heshd I hope it will be fixed soon. I opened issue about this.

@GuoChen-thlg
Copy link

When I tried to use it in a development environment, it worked fine

:root{
     --47536436-theLen: 200px;
    --47536436-angle: 45deg;
    --47536436-offset: 100px;
}
.style{
    position: relative;
    list-style-type: none;
    width: var(--47536436-theLen);
    height: var(--47536436-theLen);
    transform: rotate3d(0.7, 0.5, 0.5, var(--47536436-angle));
    transform-style: preserve-3d;
}

But when I packed it, it didn't work

:root{
   --47536436-theLen: 200px;
   --47536436-angle: 45deg;
   --47536436-offset: 100px;
}
.style{
    position: relative;
    list-style-type: none;
    width: var(--946a2296);
    height: var(--946a2296);
    transform: rotate3d(.7,.5,.5,var(--8afc983c));
    transform-style: preserve-3d;
}

@GulnazKhasanova
Copy link

Tell me how to dynamically change a property background-image in ::before?

@yyx990803
Copy link
Member Author

Looks like most of the issues raised after the final comments period were about implementation bugs - most of which have been addressed in the latest version (3.1.5).

  • One of the issues where the feature does not work in production mode for vue-cli is due to an issue of thread-loader and is tracked in using v-bind in css will have a problem in production env core#3921

  • If there are other cases where it's not working as specified in the RFC, please open an issue at the vue-next repo with reproduction instead of commenting here.

Since these are all implementation issues, the RFC itself is considered finalized. We will try to resolve all implementation bugs in the upcoming 3.2 and this feature will be out of experimental status when 3.2 stable is released.

@yyx990803 yyx990803 merged commit 0574e70 into master Jul 16, 2021
@yyx990803 yyx990803 deleted the style-vars-2 branch July 16, 2021 18:58
@bluwy bluwy mentioned this pull request Jul 17, 2021
@LiuQixuan
Copy link

How to prevent the rendered css variable with hash prefixed? The same component will render multiple css files, which is obviously unreasonable. I am not afraid of same css variable name.In my mind,the css variable was written in the HTML element and only works for the css var( ) syntax under the HTML element. Why do you need to add a hash prefix? Just to avoid duplication?

@iDerekLi
Copy link

BUG: in SCSS

System Info

 OS: Windows 11
   CPU: (8) x64 AMD Ryzen 5 3500U with Radeon Vega Mobile Gfx
   Memory: 535.72 MB / 6.94 GB
 Binaries:
   Node: 12.15.0 - C:\Program Files\nodejs\node.EXE
   Yarn: 1.22.5 - C:\Program Files (x86)\Yarn\bin\yarn.CMD
   npm: 6.13.4 - C:\Program Files\nodejs\npm.CMD
 Browsers:
   Edge: Spartan (44.22000.1.0), Chromium (91.0.864.67)
   Internet Explorer: 11.0.22000.1
 npmPackages:
   "sass": "^1.26.5",
   "sass-loader": "^8.0.2"
 vueCLI:
    @vue/cli 4.5.13

CSS

css parsed variables are normal.

<script setup>
const width = 123;
</script>
<style scoped>
.canvas {
  position: relative;
  width: v-bind(width + "px");
  box-shadow: 0 0 3px 1px rgba(0, 0, 0, 0.2);
  background: #ffffff;
}
</style>

image

SCSS

scss parsing variables are not equal.

<script setup>
const width = 123;
</script>

<style lang="scss" scoped>
.canvas {
  position: relative;
  width: v-bind(width + "px");
  box-shadow: 0 0 3px 1px rgba(0, 0, 0, 0.2);
  background: #ffffff;
}
</style>

image

@berzi
Copy link

berzi commented Sep 6, 2021

v-bind() doesn't seem to work inside CSS rules with the ::v-global() selector.

Is this the right place to report this? I got here through a warning in the compiler.

@eyun-tv
Copy link

eyun-tv commented Jan 18, 2022

v-bind() doesn't seem not work
i use pug & coffeescript and stylus

image

image

@privatenumber
Copy link

@iDerekLi @eyun-tv

In both of your examples, you're using preprocessors that manipulate the source code before passing it to Vue. That means Stylus or SCSS is likely changing the JS expression you pass inside v-bind. In the RFC, it says expressions should be wrapped in quotes, and that will probably prevent the preprocessors from interfering too: v-bind('vol + "%"')

FYI, I don't think this is the right place to make bug reports or seek help, so ideally you submit an issue instead of responding here.

@eyun-tv
Copy link

eyun-tv commented Jan 22, 2022

image
I don't think that's the problem you're talking about
I made the changes as you said, but it still doesn't work
I see that the variable is generated in the generated css, but there is no place to initialize this variable @privatenumber

@andrei-gheorghiu
Copy link

@eyun-tv, according to this post above, you should not report implementation bugs on this thread. Please create a separate issue on the vue repo.

Also note, starting tomorrow, vue will default to version 3. You might want to wait until the move is completed and post on the appropriate renamed repo.

@L1yp
Copy link

L1yp commented Nov 21, 2022

@eyun-tv, according to this post above, you should not report implementation bugs on this thread. Please create a separate issue on the vue repo.

Also note, starting tomorrow, vue will default to version 3. You might want to wait until the move is completed and post on the appropriate renamed repo.

same problem, I guess it was caused by teleported.

@L1yp
Copy link

L1yp commented Nov 21, 2022

repo url

@deepthan
Copy link

deepthan commented Jun 15, 2023

Post a method, have not found for a long time to try out, I hope it can help you.

css background: src(xxx) uses variables to concatenate urls in js

<script setup lang="ts">
const imgUrl = ref(
  'url(https://p3-passport.byteimg.com/img/mosaic-legacy/3795/3047680722~180x180.awebp)'
)
</script>

<style lang="less" scoped>
.compass {
  background: v-bind(imgUrl);
}
</style>

@deepthan
Copy link

deepthan commented Jun 15, 2023

How can i to bind a variable of @font-face src ? The following does not work.

<script setup lang="ts">
const fontFamily = ref(
  'url(http://www.xxx.comxxx.ttf)'
)
</script>

<style lang="less">
.@font-face {
  font-family: 'YouSheBiaoTiHei';
  src: v-bind(fontFamily);
}
</style>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
final comments This RFC is in final comments period sfc Single File Components
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet