-
Notifications
You must be signed in to change notification settings - Fork 81
Allow parent to impact child component CSS #22
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
Conversation
While I do reckon there is a lot of demand for this, I think having the One of the main reasons to work with components is re-usability and portability, but also a delegation of responsibilities. Adding a component should be as easy as simply adding the component without having to know the inner workings (or markup) of this component. A consumer should only be aware of the properties, methods and events of a component. In order to style a child component one has to be aware of the markup as well, which violates this 'delegation of responsibility'-principle. This also opens the possibility of many unwanted effects when working in teams or with distributed component libraries. A simple example of this would be a button component that renders as follows:
If a consumer decided to change the styling slightly by using:
This will all of suddenly not work anymore when (for whatever reason, this is just an example) the people responsible for the button change the markup to
As you can see, this would mean that a change in a component is potentially no longer isolated to that component, and the entire codebase should be checked to see if anybody has been overriding the default styles. In larger setups this could quickly become an impediment to development. Hence (again: in my opinion) a bad practice. The |
For me, the desired use case is not to style a deeply nested child. |
@stephane-vanraes I understand what you're getting at, but we shouldn't miss out on useful features just because a few developers will abuse it... Generic Link componentImagine I have generic <a
href={href}
class="link"
class:active={active}
bind:this={anchor}
on:click={onClick}>
<slot/>
</a>
<script>
let router = getContext('router')
let anchor
let href = ''
$: active = router.currentRoute.url === href
async function onClick (event) {
event.preventDefault()
await router.push(anchor.getAttribute(href))
}
</script> This would be a component that is meant to be styled by its parents, and it would make no logical sense to have every possible type of style available inside the Link component. If I wanted to turn one of these anchor tags into a button, another into a nav-link, and another into a footer link, I should be able to. There's no reason that there shouldn't be a feature to treat a component like a normal HTML element.
|
👎 <style lang="stylus">
import 'normalize.styl' //can't make 'normalize' file contents global
</style> 👍 <style lang="stylus" global>
import 'normalise.styl'
</style> |
For SCSS, it is still possible to maintain some degree of structure with nested .root {
:global(.child) {
:global(.desc1) {} // .root :global(.child .desc1)
& > :global(.desc2) {} // .root :global(.child > .desc2)
}
} The above compiles just fine and works as expected. However, I do agree that this is verbose. As for the claim about re-usability, portability and delegation of responsibilities, I believe that this is undesirable in some cases. Pages are components too, and those are not intended to be portable, they are already as large as it gets, and they could benefit strongly from having control over actual reusable components used inside. |
To me the suggestion for an attribute on the style tag looks like a really bad solution to this 'problem'. Once that's above the fold it's no longer obvious that your style tag is being applied globally and it will be far too easy to unwittingly add CSS to the wrong style tag. Your example with the generic link component is also easily solved by using components within the slot that apply styling directly; or just adding the required styling in a global CSS file. I had to spend years working with 'global' (i.e. standard) CSS and have never understood the problem people appear to have with managing scope 🤷 |
@blindfish3 It's not the only suggestion.
I've never had this happen to me in Vue. Just because a developer might be stupid with the feature doesn't mean it should be blocked. With the generic link component, you can't use a Please show me an example of how you would accomplish that in svelte currently. Free to discuss the problem with you in Discord |
Would also be great to support a more versatile version of the <style>
.scoped-style {}
@global {
import 'global-stylesheet';
.global-style {}
}
</style> In this way we remedy the problem of not being able to use <style lang="scss">
.something-scoped {
@global {
.child-component {
.descendant-component {}
}
}
}
</style> This can even be part of Benefits
@global
.childComponent
background: blue;
h1
color: red;
// versus
:global(.childComponent)
background: blue;
:global(.childComponent h1)
color: red;
.root {
@global {
.child {
.desc1 {}
& > .desc2 {}
}
}
}
// versus
.root {
:global(.child) {
:global(.desc1) {} // .root :global(.child .desc1)
& > :global(.desc2) {} // .root :global(.child > .desc2)
}
}
|
@illright I love the above approach, it would solve a LOT of problems. Seems technically possible at first-glance, and resolves many of the concerns that @blindfish3 had. EDIT: @illright's solution has really grown on me. It retains syntax highlightingIt supports nesting//stylus
@global
.childComponent
background: blue;
h1
color: red; Easier to read for people that want to use nesting@global {
.childComponent {
background: blue;
h1 {
color: red;
}
}
} :global(.childComponent){
background: blue;
}
:global(.childComponent h1){
color: red;
} Doesn't need to repeat
|
For a second thought, maybe going with Complying with the CSS modules spec would mean that there already exists a working Stylelint plugin to lint this new addition: https://github.com/pascalduez/stylelint-config-css-modules |
div :global {
.foo {
color: blue;
}
}
div :global .bar {
color: red;
} would transpile to the equivalent of div :global(.foo){
color: blue;
}
div :global(.bar){
color: red;
} |
Meanwhile, the |
Lovely, would be great if we could get this added to svelte too 🙏 as not all people may be using svelte-preprocess.
|
Supporting a point that When using Sass (or any other nesting-enabled CSS preprocessor), one might write code like this: .local {
color: red;
// wrapped in a local selector to prevent style bleed
:global(.one, .two) {
color: red;
}
} Expected output: .local.svelte-xxx {
color: red;
}
.local.svelte-xxx .one,
.local.svelte-xxx .two {
color: red;
} Actual output: .local.svelte-xxx {
color: red;
}
.local.svelte-xxx .one,
.two {
color: red;
} This happened because Sass was unable to split Currently, the documentation of |
Just how we have Example: <script>
import CustomComponent from './CustomComponent.svelte';
</script>
<style>
.border {
border: 2px solid black;
}
</style>
<CustomComponent class="{$$classes.border}"></CustomComponent> The above doesn't work for another unfortunate reason, it's not possible to write <script>
import LibOneComponent from 'lib-one/Component.svelte';
import LibTwoComponent from 'lib-two/Component.svelte';
import LibThreeComponent from 'lib-three/Component.svelte';
</script>
<style>
.border {
border: 2px solid black;
}
</style>
<!-- without a standard for passing hashed class names everyone will chose random
prop names and users will have to deal with painful scenarios like this one -->
<LibOneComponent cssClass="{$$classes.border}"></LibOneComponent>
<LibTwoComponent classNames="{$$classes.border}"></LibTwoComponent>
<LibThreeComponent classStyle="{$$classes.border}"></LibThreeComponent> Ideally accepting a prop by the name of |
@pretzelhammer You can actually use a
Repl: https://svelte.dev/repl/a83454b0e38045708c72e0660b02e1e2?version=3.24.0 |
@pretzelhammer honestly, it's not clear to me how the |
The problem with the |
@illright Yes. Both React and Vue solved this problem years ago. Devs need to be to access the hashed classnames in JS as it enables useful patterns like passing scoped styles from a parent to a child without leaking anything into the global scope.
@stephane-vanraes No, I don't think <style>
.some-class {} /* hashes to .some-class-3eu983e */
.some-other-class {} /* hashes to .some-other-class-mb323 */
</style> Then I would expect this: $$classes["some-class"] // returns "some-class-3eu983e"
$$classes["some-other-class"] // returns "some-other-class-mb323" |
@pretzelhammer but as far as I know, in Svelte it's more like a single hash for the whole component, and the classnames aren't altered. So if the component had styles: <style>
.class1 { color: red; }
.class2 { color: blue; }
</style> then the style output would be: .class1.svelte-xxx { color: red } .class2.svelte-xxx { color: blue } So the That said, we could instead opt in specific components into the stylesheet, with some property like Component.svelte <style>.foo { color: red }</style>
<div class="foo" /> Parent.svelte <style>.foo { color: blue }</style>
<div class="foo" />
<Component svelte:styled /> And then the rendered output could be: <style>
.foo.svelte-yyy { color: red }
.foo.svelte-xxx { color: blue }
</style>
<div class="foo svelte-xxx"></div>
<div class="foo svelte-yyy svelte-xxx"></div> Notice how passing the |
@illright If I'm using a component UI library in my app then
As far as I know that's an implementation detail and not a public interface. I don't see why Svelte couldn't change to just hashing the classnames. |
@pretzelhammer mangling classnames is quite a breaking change. Right now, a lot of existing code is relying on the scoped global (i.e. $$classes['some-class'] === 'some-class svelte-xxx-yyy' where |
Has any consideration been given to a new 'Component directive' that could look something like: <script>
import Child from './child.svelte';
</script>
<style>
.parent {
display: flex;
flex-direction: row;
}
.parent > .child {
flex: 0 0 initial;
}
</style>
<div class="parent">
<Child attr:class|append="child"></Child>
</div> This would be a bit of a blunt instrument that would happily append the |
@ggoodman And how would one style non-top-level elements of the |
I didn't consider that requirement and think that might be both dangerous and surprising for users. My use-case is limited to parent-child relationships so "going deeper" wasn't motivated by any need. What kind of use-case have you seen to peek behind the curtain like that @illright? |
@ggoodman Regarding "dangerous" and "surprising", practically everything in this thread can be categorized this way :p As for the usecase for going deeper with styling – as I've mentioned earlier here, components like Sapper pages, which are final destinations and not meant to be portable, could really benefit from having full control over what happens with their children. Consider some component library that you're using inside a page component. There are many cases where you'd want to, say, adjust some spacing inside the library component to fit your page better |
Thanks for clarifying that makes a lot of sense! OK, new idea inspired by some of the things that can be done in styled-components and friends...
<script>
import Child from './child.svelte';
</script>
<style>
.parent {
display: flex;
flex-direction: row;
}
.parent > :component(Child) {
flex: 0 0 initial;
}
</style>
<div class="parent">
<Child></Child>
</div> Would render as: <div class="svelte-12345 parent">
<div class="svelte-67890 ..."></div>
</div> .parent.svelte-12345 {
display: flex;
flex-direction: row;
}
.parent.svelte-12345 > .svelte-67890 {
flex: 0 0 initial;
} I think that this might be on the path to addressing both deep and immediate parent-child relationships. Since it would be svelte-specific syntax (that appears to NOT be problematic for language tools), the compiler could convert this to an optimized form as shown. Because this is 'using the platform' (of css), deeper selectors could easily be supported. |
This would be pretty neat with Those 'style' components, when mounted would inject global styles that would affect arbitrary referenced components 💥 . Does this seem to address your use-case @illright? |
@ggoodman consider the following scenario: Component.svelte: <div id="one" />
<div id="two" /> App.svelte: <script>
import Component from 'path';
</script>
<style>
main > :component(Component) {
color: red;
}
</style>
<main><Component /></main> The problem here is – which element gets P.S. I didn't get what you meant by the picker and style components, how would that work and what problem would that address |
I bet we could come up with some opinion that the compiler would be able to either enforce or complain about when targeting multi-root components. Actually targeting div1 or div2 could use |
@DominusVilicus Thanks for creating this RFC and thanks to everyone for the discussion, most of which seems to centre around improving the ergonomics of some of the solutions proposed in the RFC. The problem of styling or theming child components has come up many times before, as illustrated by the many linked issues. While there are solutions to this problem, there is not yet an API that makes this process easy, only an escape-hatch via Most of the linked issues, as well as this RFC, attempt to solve this problem by relaxing Svelte's CSS scoping rules, providing a better API with which to use global, or by manually passing down classes. We have never found this to be an acceptable solution which is why those issues have been closed. That position has not changed. Various reasons have been given both in the chatroom and in issues, but I will address this in full here. Svelte is an opinionated tool; it has opinions about how things should be done and what should be allowed. By adding constraints, we have managed to create a simple API and a performant output. These are often conscious decisions: we don't necessarily agree with historic approaches or how other tools are doing things, and we are happy to push back where we think there may be a better way. This is one of those cases, and I feel that context is important here. The ProblemCSS encapsulation is importantCSS encapsulation is a critical feature of single file components in Svelte; it allows you to think only about the styles that live together in a given component. Managing CSS has long been one of the more challenging aspects of building for the web; we have no desire to bring those problems back via official APIs that encourage the de-scoping of CSS. We do not wish to revisit the age of namespaced CSS selectors and required preprocessors. There is an escape hatch in Make the right thing easy, and the wrong thing possible. Components are their own bossA component should be in complete control of itself. Not only should a component's styles not leak out but other component's style should never leak in. Consider this 'Encapsulation part 2' if you will. When writing a component, you have certain guarantees that not only will the styles you write be contained within the component, but nothing from the outside should affect you either. You have a certain confidence that if it works in isolation, then it will continue to work when embedded within a complex application. There are tools in Svelte that break this expectation to a degree, but they are a bit annoying to use, which makes it an active decision on the part of the developer. The API hints at the way we want you to do things because we feel that this will give the better experience. If a component is to accept styles from outside, then that should be part of the component's API, however that is expressed. It could be props, or it could be css variables, or it could be something else we haven't thought of yet. What we don't want to happen is to bless an approach that inverts this control, allowing an arbitrary parent to impact the inner-workings of a component. We also believe that this explicit API should be relatively granular and work at the selector or value level, not at the element level. This is why we think classes are "a blunt instrument", as Rich put it. Exposing even a single class gives you a clear entry point to the entire component's markup allowing complete control over every aspect. We need a more granular way for a component to define an explicit style interface. The proposals are mostly sugarMost of these proposals relating to The And they don't solve the biggest issue with
|
I want to ask about this because I'm surprised I haven't seen anyone challenge it. I published svelte-headlessui, a port of a very popular React/Vue component library. These are components which are intentionally unstyled. Users are supposed to style them completely. This passage feels to me like it's saying (pretty explicitly) that this is not what a Svelte component should be and that this kind of component is discouraged philosophically by the Svelte team. Is that fair to say? My problem with the status quo is that your "components are their own boss" principle doesn't allow for a component to choose to delegate its styling to callers. I think my library is a great example of when a component author would want to do that, but I think there are many other cases too (hence all the interest in this and related RFCs and feature requests, etc). It's a real shame, because on a technical level this problem should be solvable--there just needs to be a way for components to opt in to receiving the scoping class from their parent. The problem seems to be purely ideological. I understand where your point of view comes from (a desire to enforce strict separation at component boundaries, preventing people from doing bad things) but it feels like it's based upon a too narrow idea of what a component can be. As it is, I'm more or less giving up on the Svelte |
Rendered
Related issues: