From cc244ca0290a93396da4eb325d67c7b400a458ea Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 12 Nov 2019 15:59:45 -0500 Subject: [PATCH 1/8] style properties RFC --- text/0000-passing-custom-properties.md | 273 +++++++++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 text/0000-passing-custom-properties.md diff --git a/text/0000-passing-custom-properties.md b/text/0000-passing-custom-properties.md new file mode 100644 index 0000000..c94d1cd --- /dev/null +++ b/text/0000-passing-custom-properties.md @@ -0,0 +1,273 @@ +- Start Date: 2019-11-12 +- RFC PR: (leave this empty) +- Svelte Issue: (leave this empty) + +# Passing CSS custom properties to components + + +## Summary + +This RFC proposes an idiomatic way to pass styles to components for the purposes of theming, using CSS custom properties: + +```html + +``` + + +## Motivation + +Theming components is currently too difficult. Suppose you were using the `` component from the (fictional) Potato Design System. Perhaps it has markup like this: + +```html + + + + + + + +``` + +(For brevity I'm omitting `tabindex`, `role` and various `aria-*` attributes that a real slider component would include for a11y reasons.) + +Suppose further that we want to control (say) the colour of the rail and the track, or the size of the thumb. At present, there are only bad options: + + +### Global CSS + +Because Svelte deliberately leaves class names intact, it's possible to select `.potato-slider-rail` etc from any stylesheet. But while this can be a useful escape hatch, it's not something to encourage as a method for theming, since it breaks encapsulation (the component has no control over which aspects of its styles can be externally controlled, even while it has complete control over which values are props and which are internal state) and is likely to lead to broken or buggy styles. + +It's also brittle, since it treats internal (potentially changeable) implementation details as a public API. + +Furthermore, it becomes more difficult to style components differently based on where they appear in an app. + + +### The `:global` modifier + +A consuming component can target the same internal class names (or any other selector) using `:global(...)`, optionally inside a non-global selector to restrict the styles to children of that component. + +This has all of the downsides of global CSS (except being able to style different instances of a component differently) plus more: it may result in the need for additional DOM, and... it's kinda ugly. It feels like a hack. + + +### Props + +An alternative approach is for the component to expose props that are then used for styling: + +```html + + + + + + + + + +``` + +There's a few things not to like about this. Firstly, we're conflating state and styles. Secondly, we have to overuse inline styles to apply those values, and the compiler has to generate extra code to make that possible. + +Thirdly, this provides no good way to control theming at an app level. Every time `` is used, its style properties must be repeated. In most cases, that's probably not what we want. + + +## Detailed design + +Style properties — handily distinguishable from regular properties by the leading `--` used by CSS custom properties — are passed down to components through a separate channel. The example at the top of this RFC might be converted to the following JavaScript: + +```js +const slider = new Slider({ + props: { + value: ctx.value, + min: 0, + max: 100 + }, + styles: { + '--rail-color': 'black', + '--track-color': 'red', + } +}); +``` + +These properties — `--rail-color` and `--track-color` etc — are essentially part of the interface of ``, just like the `value`, `max` and `min` properties are. + +Inside the Slider component, these styles would need to be applied to the top-level element — the equivalent of doing this: + +```html + +``` + +In practice that might look something like this: + +```js +// internal helper +function apply_styles(node, styles) { + for (const k in styles) { + node.style.setProperty(k, styles[k]); + } +} + +function create_main_fragment(ctx) { + // ... + + return { + c() { + span = element("span"); + apply_styles(span, ctx.$$styles); + }, + // ... + p(changed, ctx) { + if (changed.$$styles) apply_styles(span, ctx.$$styles); + }, + // ... + }; +} +``` + +(It would get slightly more complex for cases where there was an existing `style` attribute, particularly if it included a `--rail-color` property or was an opaque `style={styles}` type attribute. But the principle is the same.) + +In the SSR case: + +```js +return `...`; +``` + + +### Inheritance + +In the same way that custom properties applied to an element affect all descendant elements, custom properties applied to a component would affect all DOM within that component. It could be argued that this breaks encapsulation, but a) it matches the expectations people have from using custom properties to date, b) would in general be more convenient, and c) is more or less out of our hands since that's just how custom properties work. + + +### Multiple top-level elements + +A component might have multiple top-level elements. In such cases, it would be necessary to apply the custom properties to all of them. + +We could consider omitting them for elements that appear not to use the custom properties, and don't contain any components or any child elements that use them, but this might break expectations: it would be valid to select an element using global (or `:global`) CSS and expect the custom property to be present. + + +### Zero top-level elements + +A trickier case is when there are no top-level elements, only components without any surrounding DOM. In these situations we would need to forward styles: + +```html + +``` + +```js +const toplevelcomponent = new TopLevelComponent({ + props: {...}, + styles: Object.assign({}, ctx.$$styles, { + '--foo': ctx.bar + }) +}); +``` + + +### Global theming + +Because this approach uses custom properties, it becomes straightforward to set theme styles globally, or for a large subtree of the app: + +```css +/* global.css */ +html { + --rail-color: black; + --track-color: red; +} +``` + +```html + +
+ +
+ + +``` + +A consumer of the component can override them... + +```html + +``` + +...but ultimately the component itself has control over what is exposed, and can specify its own fallback values using normal CSS custom property syntax: + +```html + + +``` + + +## How we teach this + +This ought to be straightforward to teach, at least to people already familiar with custom properties. Terminology-wise, it probably makes sense to refer to 'style props' to distinguish them from regular component props. + +It would require a new tutorial chapter and updated documentation. + + +## Drawbacks + +*Technically* this would be a breaking change, since `--foo` is currently passed down as a regular prop (albeit only accessible via `$$props['--foo']`). I feel pretty confident saying this isn't a real world concern. + +There are some more salient drawbacks: + +* It relies on something that is inherently global. Different components might 'claim' a given property name. While it's possible to differentiate them at the subtree level, it's not possible to do so globally. +* The compiler would need to generate extra code for every component (for applying received style properties, and for passing them on to top-level child components), regardless of whether they were actually used, and the helper would be included in everyone's app. (This *could* be avoided with some form of whole-app optimisation.) +* IE11 doesn't support custom properties, so we'd be pushing the ecosystem towards incompatibility with that browser. Should we care? Probably not. Component authors who wanted to support IE11 would have to provide fallback values, and consumers of those components would have to be okay with those fallbacks. +* Regular component properties are statically analysable, which could one day allow us to have typechecking and autocompletion when using those components. The same isn't true for style properties. We could imagine some way of changing that (some syntax that lives inside, or on, the ` +``` + +Aside from being an implementation nightmare, I think the proposal in this RFC is *strictly better* than props-in-style — it gives you the same expressive power in a neater, more idiomatic way, along with the global theming ability. + + +## Unresolved questions + +I'm not a big design system user, so I would very much like to get feedback from people who are. Would this solve your problems? What have we missed? \ No newline at end of file From e1589920153d75e5b90e56a8e5a3020dc0c3254f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 12 Nov 2019 16:00:03 -0500 Subject: [PATCH 2/8] rename file --- ...0000-passing-custom-properties.md => 0000-style-properties.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename text/{0000-passing-custom-properties.md => 0000-style-properties.md} (100%) diff --git a/text/0000-passing-custom-properties.md b/text/0000-style-properties.md similarity index 100% rename from text/0000-passing-custom-properties.md rename to text/0000-style-properties.md From 272befa6b916dc4248e0d34190784d6b50006559 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 12 Nov 2019 16:41:19 -0500 Subject: [PATCH 3/8] add style="..." prop alternative --- text/0000-style-properties.md | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/text/0000-style-properties.md b/text/0000-style-properties.md index c94d1cd..6214486 100644 --- a/text/0000-style-properties.md +++ b/text/0000-style-properties.md @@ -237,7 +237,13 @@ There are some more salient drawbacks: ## Alternatives -One alternative is to do nothing: expect people to continue using `:global` and friends for this purpose. It's got us this far. + +### Do nothing + +Expect people to continue using `:global` and friends for this purpose. It's got us this far. + + +### Do nothing, but encourage the use of custom properties We could also encourage the use of CSS custom properties for theming without making any changes to Svelte itself, in which case people could get the same end result by wrapping their components in elements that provide custom properties: @@ -249,8 +255,25 @@ We could also encourage the use of CSS custom properties for theming without mak This has the merit of simplicity and obviousness, and doesn't involve any extra code being generated, but it's also a hack: it signals that we don't consider component themeability to be a problem worth solving properly. + +### Do nothing, but encourage `style` forwarding + +If component authors were in the habit of expecting a `style` prop, and applying it to top-level elements, we could do the same thing like so: + +```html + +``` + +This arguably breaks encapsulation (it's not encouraging you to only pass down custom properties, but *any* styles to an element you don't control), and is contingent on component authors handling it in a consistent way. + + +### Special-case `class` + Another suggestion is to special-case the `class` property, per [#2888](https://github.com/sveltejs/svelte/pull/2888). This is arguably more in line with popular CSS-in-JS solutions. Personally, I think `class` is too blunt an instrument — it breaks encapsulation, allowing component consumers to change styles that they probably shouldn't, while also denying them a predictable interface for targeting individual styles, or setting theme properties globally. + +### props-in-style + Something else that comes up from time to time is the idea of supporting `{props}` directly in the `