-
Notifications
You must be signed in to change notification settings - Fork 82
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
Passing CSS custom properties to components #13
Conversation
combining alternatives, can we recommend making the component support style= property for theming, and that authors pass styles through to whatever real dom element(s) it sees fit?
|
Hijacking something that's already a valid prop like Actually, I guess technically this RFC is a breaking change, but the probability that someone would be using a prop starting with belated edit: I see that this 'technically breaking' thing was actually already acknowledged in the RFC and dismissed, which I think is quite reasonable. |
I think the suggestion was for component authors to be in the habit of doing this: <script>
export let style = '';
</script>
<div style="{style}; color: purple">...</div> I've added it to the Alternatives section in the RFC |
I think I'm too worried about the explosion of variables, but that's what strikes me the most. Even in the simple example used in the rfc, we're already missing at least a |
This is the first instance we have of a property that gets auto-applied to child dom nodes. There are a couple of other use cases for this behaviour such as, spreading Since the style is going to generate a bunch of code for each element already, could we generalize this somehow? instead of style, either have some custom prefix or prop name to specify props to be applied to dom children <Component $slot="name"/>
<!-- or -->
<Component $childprops={{ slot: "name", class: "active", style; "--track-color: blue}}> This would allow a more straight forward port of some other framework's components such as vue that always have a root element and passes down attributes |
Not sure I like this PR more than |
Despite being personally attacked, I'm mostly on board with this rfc. The thing that strikes me the most is that it feels reversed in some ways, almost implicit. The parent decides what to pass down and the child component can use it if they want to. Control is still in the hands of the child, which is a good thing but the 'style interface' feels more implicit than explicit. Maybe this is a reasonable trade-off. Defining an explicit style interface would be cool but I'm not sure how it would work, it would certainly mean taking an approach to styles similar to our approach in the template, Something like this in the child: <div>
<p><a href="" />Hi<a/></p>
</div>
<style>
@expose {
--thing-color: 'blue';
--thing-hover-color: 'pink';
--thing-active-visited: 'red';
};
a {
color: var(--thing-color);
}
a:hover {
color: var(--thing-hover-color);
}
a:visited {
color: var(--thing-visited-color)
}
</style> Which would allow only those properties to be passed in by a parent and used anywhere in the component, subsequent children would probably need to define an interface as well. This maintains encapsulation and is an explicit interface but it is definitely more limiting: <Child --thing-color="cyan" --thing-hover-color="magenta"/> One question I have about the RFC, would there be a specific way to define defaults for these css properties in case one wasn't passed down? If we define them in the component directly then surely the child would overwrite the passed-down values? @rjharmon Another issue with just using @mrkishi This is true to an extent no matter what route we choose, is the specific concern just a lot of styling being done directly in the template? I figure that spreading an object of css properties could be possible, maybe this would at least improve the ergonomics. <script>
import Child from './Child.svelte';
const styles = {
"--thing-color": 'blue',
"--thing-hover-color": 'red',
"--thing-inactive-color": 'pink',
"--thing-border-width": '10px',
}
</script>
<Child {...styles} /> |
I think putting the css properties directly on the component can get crazy quite quickly. Maybe it could be done in combination with something like this (for components that require much more extensive styling): <style component="Slider">
.potato-slider {
color: green;
...
}
.potato-slider-rail {
background: rebeccapurple;
...
}
</style> The idea is to have the classes merge properties with the classes in the child thus giving the parent full control of all styles while maintaining a somewhat readable HTML. |
@halfnelson I worry about that being a bit of a Pandora's box — it throws encapsulation completely out the window. The nature of CSS custom properties is that they're inert unless the child chooses to do something with them, which wouldn't be the case for other things.
Yes, in this situation the <div style="--fg: red">
...
</div>
<style>
div {
color: var(--fg);
background-color: var(--bg, 'black');
}
</style> |
FYI svelte-select handles custom properties (CSS variables) like this... |
@kevmodrome That has the drawback of breaking encapsulation, and relying on volatile implementation details. You could still define custom properties in <div>
<Slider bind:value/>
</div>
<style>
div {
--rail-color: black;
--thumb-color: red;
}
</style> |
@rob-balfre perfect, it's already an ecosystem convention! (hopefully the camelCase doesn't catch on though 😛 ). So in essence, this RFC would enable the last example in that demo to change from <style>
.themed {
--border: 3px solid blue;
--borderRadius: 10px;
--placeholderColor: blue;
}
</style>
<div class="themed">
<h2>Theming</h2>
<Select {items}></Select>
</div> to <h2>Theming</h2>
<Select
{items}
--border="3px solid blue"
--borderRadius="10px"
--placeholderColor="blue"
></Select> |
That makes sense. :) What happens if there are 15 properties to style though (I suppose one could argue that should never happen in the first place, but still)? Wrapping the component probably is a workable solution, it just clutters up the code a bit. Personally I would want that away from the HTML (but then I'm a bit biased since I am a big fan of the styled components way of doing it). |
for completeness I am adding a comment on how this can be currently achieved with Context with downside it adds some boilerplate to components that want to be styled this way, on the up side it doesn't impact the code generated for intermediate components Inherited <WithStyles --rail-color="black" --trac-color="red">.
..bunch of tags nested
<Slider />
</WithStyles>
direct <Slider styles="--rail-color="blue" /> in slider <script>
import { inheritedStyles } from 'with-styles'
export let styles;
$: all_styles = inheritedStyles.apply(styles)
</script>
<div class="potato-track" style={$all_styles[--track-color]} />
|
@kevmodrome I'd expect that at that point you'd probably want those styles to be themed in a global.css file or similar — I'm anticipating this mainly being for limited, targeted overrides. But it's definitely possible that my imagination here is lacking |
@Rich-Harris ❤️🐪 Looks good but what other main benefits does it bring other than removing clutter? I presume the compiler would output the styles as custom properties? If thats the case then postCSS can handle IE11 support. |
Not exactly — it would apply them to top-level elements at runtime, using |
@Rich-Harris I'm not sure about that. With many design systems there will be a vareity of themes that are applied declaratively directly in the template. I dont know what that would look like in svelte but in other libraries you might do something like: import { theme } from './themes.js';
export default () => (
<SystemProvider theme={theme}>
<Thing>Hello World!</Thing>
</SystemProvider>
); But something similar might be possible with the RFC proposal. |
Hmm IE11 support is a must have for us - wish it wasn't but 30% of our clients (big corps etc) are still stuck on it. So with your approach you couldn't emit the CSS with the custom properties included? |
The CSS would reference the custom properties, but they wouldn't have values (since those are set at runtime). IOW you'd be left with a) the fallback values, or b) whatever your preprocessor replaced the custom property references with (which is a totally legitimate solution that I think we should encourage, in situations where you know the values ahead of time) |
If root component doesn't define |
@Rich-Harris for more flexible solution something like that: <script>
import Slider from './slider.svelte';
</script>
<Slider />
<style data-scope="Slider">
.rail { color: blue; }
.rail:before { content: ''; display: block; ... }
.track:hover { opacity: 0.5; }
</style> under the hood it works like Object.assign: |
This is clever and is something I've been hoping for.
|
Currently for IE11 support we use https://github.com/postcss/postcss-custom-properties |
TIL I learnt about CSS Houdini ... https://youtu.be/-oyeaIirVC0?t=894 (Chrome dev conference) |
Since the Should we update the description to point to new URL?: |
Replying to "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?" I still have a problem with styling elements that this doesn't solve. This problem is not strictly styling since you might run into this for other reasons. I'm trying to implement a material ripple. My goal is to make this ripple component completely stylable by the consumer. The easiest solution is to just forward the styles attribute of my top component. It works well but then the consumer can't use classes to style the component, they have to inline everything, and cant use any attribute of the component, although they could just wrap it with a div or a span for that. My current solution is to use:an action. So the consumer can import my ripple styling globally and do |
Sorry guys, maybe I would be silly again, but I want to be sure we really need this RFC in the way it has been proposed: Or maybe, we can get the same feature much cheaper and explicit in user-land without any core-changes already now? Of course, even in this way a huge number of side-effects still will be there, but at least it will be explicit: |
As the author of a UI library, I would say this is unnecessary as per @PaulMaly's design. That being said, I would love to be able to target a component with a class name without relying on something like this: <div>
<MyComponent class="my-class-name" />
</div>
<style>
* :global(.my-class-name) {
width: 200px;
}
</style> First of all, I have to put the Component under an element, or I can pollute things outside of my component with the same class name. Second, if MyComponent renders anything underneath it that uses the "my-class-name" class internally, it also receives the styles. Third, it is super confusing for new users, and a frequent pain point for onboarding users to my (or I assume anyone's) UI library. |
Solution! Is it visible enough?We can just end this suffering at the cost of scoping for non-class selectors and meet the consequences of class-passing, whatever it might be. There's a cssModules scoping preprocessor, which allows to:
Try itInstall it: npm i -D svelte-preprocess-cssmodules svelte-as-markup-preprocessor Adjust preprocess config in the bundler: const cssModules = require('svelte-preprocess-cssmodules');
const sveltePreprocess = require('svelte-preprocess');
const { asMarkupPreprocessor } = require('svelte-as-markup-preprocessor');
// ...
preprocess: [
asMarkupPreprocessor([
sveltePreprocess()
]),
cssModules()
],
// ... Use it: <button class="btn {$$restProps.class || ''}"><slot /></button>
<!-- something else below --> <div>
<p class="future-is-bright">The future is bright</p>
<Button class="margin-top">Click me!</Button>
</div>
<style lang="scss" module>
.future-is-bright {
color: bright;
}
.margin-top {
margin-top: 6px;
// whatever you want
}
</style> |
Remove --custom-css-propeties during Component attribute transformations. They have no property which they can be checked against and the leading dashes are invalid JSX Related RFC: sveltejs/rfcs#13
Remove --custom-css-propeties during Component attribute transformations. They have no property which they can be checked against and the leading dashes are invalid JSX Related RFC: sveltejs/rfcs#13
In 2024, is there documentation on the best practice usage of what is currently implemented? I want to use a theming system but I'm a bit confused on what the final say for this issue was, and what that looks like in practice. Tutorials online from third parties all use different methods. |
Anyone know how to get this working with svelte 5 #13 (comment) ? Or an alternative way of doing the same ? |
While I'm here, here's my feedback on this topic. I apologise in advance, this is gonna be rather extensive so you might want to buckle your seat belts. I'll start by stating I totally agree with the fact that parent components shouldn't be defining custom styles on children, but as someone having spent a lot of time with native HTML / CSS or Vue, I have a few use cases that can greatly benefit from parent being able to pass a scoped class to a child. Margins and positionsMargins and positions fall into weird category that I wouldn't consider as "styling" the child component. It's only where the component should be placed relative to other elements. I've seen suggestions where margins between elements can be replaced with a <div class="home">
<Cover class="cover"/>
<TitleBlock class="title-block"/>
<Intro class="intro"/>
<TitleBlock class="title-block"/>
<ArticleList class="articles"/>
<Footer class="footer"/>
</div>
<style>
.cover {
margin-bottom: 200px;
@media (min-width: 415px and max-width: 768px) {
margin-bottom: 150px;
}
@media (max-width: 414px) {
margin-bottom: 100px;
}
}
.title-block {
margin-bottom: 50px;
@media (max-width: 414px) {
margin-bottom: 20px;
}
}
.intro {
margin-bottom: 100px;
}
.articles {
margin-bottom: 100px;
}
</style> Another example would be where an elements needs to be absolutely positioned somewhere. <div class="modal">
<CloseButton class="close"/>
</div>
<style>
.close {
position: absolute;
top: 20px;
right: 20px;
}
</style> The current workarounds for these would be to add a wrapper around each element, which works fine but means extra HTML elements that could effectively be avoided. There is also the possibility of each component exposing each of these properties as props, but as soon as we add responsiveness into the equation it requires a solution with a load of reactive states reading the screen size. A lot more complicated and ressource intensive than a few media queries. ThemingI've been trying to find an efficient way to apply different colours to specific children but have been hitting quite a few roadblocks. The current solution of being able to pass CSS variables as props is an OK solution as long as you don't need any values to change. But in any case I believe styling should be done via CSS (it's in the name), not through Javascript. Using #13 (comment) as an example, scoped child classes would allow for this: Child <span class="slider {className}">
<input type="hidden" {value}>
<span class="rail"></span>
<span class="track" style="width: {100*value/max}%">
<span class="thumb"></span>
</span>
</span>
<style>
:root {
--railColor: white;
--trackColor: red;
}
</style> Parent <div class="form">
<Slider class="slider-1"/>
<Slider class="slider-2"/>
</div>
<style>
.slider-1 {
--railColor: white;
--trackColor: red;
}
.slider-2 {
--railColor: white;
--trackColor: red;
}
</style> This method becomes even more efficient as soon as we as any type of media query into the mix : .slider-2 {
--railColor: white;
--trackColor: red;
@media (prefers-color-scheme: dark) {
--railColor: black;
}
@media (max-width: 414px) {
--trackColor: blue;
}
} Again, doing the same with props would require Javascript to get current information from the browser and a reactive state for each of the props. Admittedly the screen size media query isn't the best example here but having different styles based on screen size may serve if you need an element to stand out on a specific device. Personal opinion timeI don't believe that any of these use cases go against the "Svelte" way of doing thing, in fact I'd ague the contrary. They give us a cleaner way of achieving what's already possible but with less HTML and less Javascript, which in turn I imagine means a performance increase. I feel like by passing props and adding wrappers we're pushing for alternative solutions to a problem that CSS has already solved, just because we don't want to allow people to style child components. Speaking of, I don't fully understand the whole "scoped classes will allow people to too easily style child components" argument. The thing is, you'd be limited to passing a single class so you can style at maximum one element. People wouldn't be able to style the entire sub tree of a child component from a parent even if they wanted to, something that the Jokes aside, I think I've seen the Sorry for the long post. I hope this may sway some opinions and that this feature could possibly be implemented some time in the future. I love that Svelte is opinionated, but the lack of scoped child classes has had me scratching my head more often than not since I've dived into the ecosystem. |
Rendered