Allow "scoped" styles to affect the "host" element of a component.
Is your feature request related to a problem?
Consider the following setup:
import { component$, useStyles$, useStylesScoped$ } from '@builder.io/qwik';
export const App = component$(() => {
useStylesScoped$(`.red-color { color: red }`);
useStyles$(`.green-border { border: solid 1px green }`);
return <p>Hello <Name class="red-color green-border"/> !</p>;
});
export const Name = component$((props: {class:string}) => {
return <span class={props.class}>PETE</span>
});
Describe the solution you'd like
I would like that the red-color styles are applied to the "host" <span> element of the <Name> component.
I understand that the styles should not leak down into the rest of the Name components template.
But the host element feels like it should be special.
Otherwise it is not possible to use useStylesScoped() to apply styles to components that live in the current template.
Describe alternatives you've considered
You can work around this by using useStyles() rather than useStylesScoped() but then the styles become global and leak everywhere.
Additional context
No response
The use case I have is that I want to provide a simple <Image> component that can be styled by applying a CSS class from the containing template.
OK so I see there was related discussion about this here: https://github.com/BuilderIO/qwik/discussions/1063 And then resolved here: https://github.com/BuilderIO/qwik/commit/d214d3950ed4d4075815bb240c581d2183f3d64a
So I think that I can get around this by combining scoped and global selectors.
For posterity the solution is to use :global(...):
import { component$, useStyles$, useStylesScoped$ } from '@builder.io/qwik';
export const App = component$(() => {
// chaining below the `p` tag ensures that the `:global` only affects descendants
useStylesScoped$(`p :global(.red-color) { color: red }`);
// here the `:global` affects the whole DOM.
useStylesScoped$(`:global(.green-border) { border: solid 1px green }`);
return <p>Hello <Name class="red-color green-border"/> !</p>;
});
export const Name = component$((props: {class:string}) => {
return <span class={props.class}>PETE</span>
});
i imagine we could provide this as well:
import { component$, useStyles$, useStylesScoped$ } from '@builder.io/qwik';
export const App = component$(() => {
const {classes} = useStylesScoped$(`.redcolor { color: red }`);
return <p>Hello <Name class={classes.redcolor} /> !</p>;
});
export const Name = component$((props: {class:string}) => {
return <span class={props.class}>PETE</span>
});
I guess the question is whether one should special case class or whether Qwik should provide a mechanism for specifying what attributes should be assigned to the host element in general?
there is no host element
i imagine we could provide this as well:
import { component$, useStyles$, useStylesScoped$ } from '@builder.io/qwik'; export const App = component$(() => { const {classes} = useStylesScoped$(`.redcolor { color: red }`); return <p>Hello <Name class={classes.redcolor} /> !</p>; }); export const Name = component$((props: {class:string}) => { return <span class={props.class}>PETE</span> });
Oh I misunderstood your suggestion. This is using CSS modules to pass the style in?
May I suggest renaming the issue to something like "allow including style scope when passing class to child component"? (since the original name is misleading because components don't have host elements)
Hmm, here's an updated playground. When you inspect the HTML output, you can see that the <p> and <Inline /> components got the extra class but the <Name/> didn't.
<body>
<!--qv q:sstyle=⭐️olukuv-0 q:id=0 q:key=Ncbm:0t_0--><style
q:style="olukuv-0"
hidden
>
.red-color.⭐️olukuv-0 { color: red }</style
><style q:style="o74rpj-1" hidden>
.green-border { border: solid 1px green }
</style>
<p class="⭐️olukuv-0" q:key="4e_3">
Hello
<!--qv q:key=4e_1--><span class="⭐️olukuv-0" q:key="4e_0"></span
><!--/qv--><!--qv q:id=1 q:key=jGuP:4e_2--><span
class="red-color green-border"
q:key="4e_4"
>PETE</span
><!--/qv-->
!
</p>
<!--/qv-->
</body>
The difference is that Name is a Qwik component, which is rendered slightly differently, in a new qwikContext, and it's not inheriting the ⭐️olukuv-0 class. I'm not sure if this is intentional or a bug.
Current behavior is as follows:
- generate scopeId
- convert the given css so each selector requires scopeId class
- give all plain elements the scopeId as a class. Qwik components don't get the class, they start a new scope
The behavior you would like changes step 3 to pass the scopeId as a class to the entire subtree. So if you use scoped styles in root, every dom element under root will have the extra class of root's scopeId.
Is that good or bad? I'm not sure, but it "feels" like that's not a great idea.
Anyway, I'm personally not a fan of scoped styles, I think modular or atomic styles are better. For modular styles you can use .module.css imports or use Vanilla Extract, and for atomic styles you can use Tailwind CSS, UnoCSS, or PandaCSS.
Now we can do like this:
export default component$(() => {
const {scopeId} = useStylesScoped(styles);
//...
return (
<SomeComponent class={scopeId} />
);
});
@genki so basically, this is the situation, right?
@petebacondarwin does that work for you?
So the idea is that you get get something back from the call to useStyledScoped$() that can be used to get access to those styles elsewhere? If so, then I think that is good enough for my case.
@petebacondarwin ok when you verified that it works for you, can you close the issue? You can try it in the playground.
Sadly this doesn't seem to be enough. The scopeId is just a string that can be used to compute the "scope" of the styles of a component. I can't work out how to use this id to apply styles to a child component.
@petebacondarwin seems to work for me?
https://qwik.builder.io/playground/#v=1.3.5&f=Q0o0JmZYE40ORv2kowBK2cQkJojnXVJTC4CeR05RBeCAADUtIO0H2%2BroAj0wS0cpvSg1NU8pttYuBagP0uzQRDUwIBEYU6DwREmktnbgpgpEEhjPNiBX6tvBTCAqgUPMry4GedQzpRZoBUblnABUpgd2IzCYEDWPbnJ%2BTn6RFbC6S88oAUtbK9QClSZoIuUbiNsgsQC1wwphGyRyCqAhAg0HBbSsBlEEDlJoyMG0g2PVRh9qx2iWHKpZEgA
import { component$, useStylesScoped$, Slot } from '@builder.io/qwik';
export const Deep = component$((p) => <div class={[p.class,"green"]}>deep</div>)
export const Parent = component$(()=><div>Parents: <Slot/></div>)
export default component$(() => {
const {scopeId} = useStylesScoped$(`
.green { background-color: lightgreen; }
`)
return <Parent>
scopeId: {scopeId}
<p class="green" >Hello Qwik</p>
<Deep class={scopeId}/>
</Parent>;
});
Fair enough. Thank you for bearing with me. Happy to close this.