-
-
Notifications
You must be signed in to change notification settings - Fork 33.7k
Description
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).