Skip to content

FR: Reactive CSS variables #7346

@richardtallent

Description

@richardtallent

What problem does this feature solve?

Often, it's desirable for a component to expose properties the caller can use to customize the style. But if these take arbitrary values rather than just, say, class names, the child component then must use style attributes to apply the values. Unfortunately, this litters the <template> markup with style information. Also, the style attribute cannot be used to assign CSS properties to pseudo-elements like :hover and :before, which significantly limits the properties a component can expose to the caller for custom styling.

The only workaround is for the parent's CSS to override the child's CSS, but that requires the parent's CSS to be coupled to the child's implementing markup. The same value (or variants based on it, such as lighter or darker shades of a color) may be used in a number of selectors, complicating this effort, and if the component uses scoped CSS or highly-specific selectors, it may be even more difficult to override.

CSS variables are now fully supported by every common browser other than IE11. So, I'm proposing that Vue support mustache syntax in the <style> block of SFCs where CSS variables are declared, and that Vue set and react to changes to these values by using the DOM's style.setProperty method.

This will allow component authors to provide props for more styling decisions, in a way that is still as reactive as using the style attribute, but with more capabilities (for pseudo-elements) and a tidier template. Internally, components can also use this to make styling decisions, including for pseudo-elements, based on computed values, all fully reactive.

Here's an example of a component that does support CSS variables as properties, but has to wire it up manually with a watcher:

https://github.com/richardtallent/vue-stars/blob/master/src/VueStars.vue

It may be possible to support IE <= 11 by replacing variables in the style with values and replacing the generated style tag as needed.

What does the proposed API look like?

The API would simply be that the <style> block accepts mustache syntax in the declaration of CSS variables, and that behind the scenes, it reacts to changes by using the style.setProperty() method to update the CSS variable's value.

From the component author's perspective:

<template>
  <h1>{{ title }}</h1>
</template>
<script>
export default {
  name: "ColorHeader",
  props: {
    bg: { type: String, default: "inherit", required: false },
    bgHover: { type: String, default: "inherit", required: false },
    title: { type: String, default: "(Untitled)", required: false },
  }
}
</script>
<style>
:root {
   --bg-color: {{ bg }};
   --bg-hover-color: {{ bgHover }};
}
  h1 { background-color: var(--bg-color); }
  h1:hover { background-color: var(--bg-hover-color); }  
</style>

One down side I see is that this would require Vue's compiler to parse the CSS so it recognizes the variable name before the mustache, which could be a problem when using CSS that needs a pre-processor. So here's an alternative implementation:

  • Vue supports mustache syntax anywhere within the style block, but it is understood that it should only be used for CSS values, not the names of properties or selectors.
  • Vue makes no attempt to parse the CSS other than to find the mustaches.
  • For each discrete mustache expression, Vue replaces the mustache syntax with a CSS variable declaration at the top (auto-named) and var(--auto-named-variable) where the mustache syntax appeared.
  • Vue then just needs to update the value for the names it created reactively.
  • This would be compatible with any CSS variant that allows CSS variable syntax (and that doesn't replace it or the mustache syntax with something else).

If mustache syntax causes too much of a headache with linters, IDEs, etc. (since curly braces are important to CSS), a third alternative would be for the component author to use the CSS variable syntax, and require that if you want Vue to reactively set and update that variable, you simply use the kebab-case version of one of your data, prop, or computed attributes (i.e., no complex expressions or direct use of methods). Example:

<template>
  <h1>{{ title }}</h1>
</template>
<script>
export default {
  name: "ColorHeader",
  props: {
    bgColor: { type: String, default: "inherit", required: false },
    bgHover: { type: String, default: "inherit", required: false },
    title: { type: String, default: "(Untitled)", required: false },
  }
}
</script>
<style>
  h1 { background-color: var(--bg-color); }
  h1:hover { background-color: var(--bg-hover); }  
</style>

To prevent collisions between real legacy variables and same-named component features, this could be opt-in with an attribute on the style element (like "scoped" operates).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions