-
Notifications
You must be signed in to change notification settings - Fork 378
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
Theming options for shadow roots #864
Comments
What are examples of user land solutions? We need to study how various libraries & frameworks are tackling this problem & list of concrete use cases so that we can evaluate each proposal properly. |
This is the most developed one that I know of: https://github.com/vaadin/vaadin-themable-mixin (cc @web-padawan) You could basically consider the And @aomarks has been looking at a new system inspired by Vaadin's themable work, |
One of the hazards with I feel like most of the objections with using it for theming could be solved by providing a mechanism to use imported names to use for styling (akin to my previous suggestion on lexical names). e.g. Consider this example that provides deep theming for syntax highlighting: /* styles.css */
@part $var-token {
deep: true;
} /* code-/register.js */
import { var, keyword } from './styles.css';
/// ...
for (const token of tokens) {
if (token.type === 'var') {
const varElement = createVarElement(token);
varElement.parts.add(var);
} else if (token.type === 'keyword') {
const keywordElement = createKeywordElement(token);
keywordElement.parts.add(keyword);
}
// ...
}
/// ... <!doctype-html>
<style>
@import "/components/code-/styles.css" {
/* Import lexically scoped part name */
$var;
$keyword;
};
/* Syntax highlighting theme */
/*
* ::theme would be deep but only work on lexically exported part names
* ::part would operate on local tree as per usual and doesn't pierce shadow roots
*/
::theme($var) {
color: yellow;
}
::theme($keyword) {
color: green;
}
</style>
<!-- This shadow root is loaded from the article file -->
<blog-post src="./how-to-program.html">
<#shadow-root>
Hello this is hello world!
<code- lang="js">
console.log("Hello world!");
</code->
</#shadow-root>
</blog-post> |
I think lexical names, while useful (the linked proposal is from me), is quite a large lift for this feature. We have existing cross-scope features that rely on string names, like CSS variables. It seems to me like we can do something opt-in with names and support lexical names if and when that ability comes to CSS.
One change discussed was just using a separate attribute, like |
I don't know how browser stylesheet internals work but it should be no harder than The implementation would basically be treat It'd be pseudo like if it was written as: ::theme(https://resolved.url/path/to/foo.js#bar) {
color: blue;
} element.parts.add(new URL('https://resolved.url/path/to/foo.js#bar')); This would just then follow the processing model of |
Regarding the user land solutions: at Vaadin we do inject styles to shadow roots:
While we use Another example of a user-land solution that is currently a prototype is Stylable util:
See also full documentation here. Of course this isn't something that we'd like to use in production today and we agree that (ab)using media queries for styling Shadow DOM is arguable and might affect performance. @jouni would you like to share the slides from your recent presentation on this BTW? |
Here’s the presentation I created to illustrate my frustrations with our existing theming solution (ThemableMixin) and how media queries could be used as a similar workaround but with a little nicer developer/designer experience: https://docs.google.com/presentation/d/1on1vav0grmtPiOsGfx0qbTIJ8ryiMMTJxOyx-jOmBhQ/edit#slide=id.g6b7ea3c84b_0_103 |
I have some concerns regarding How is it supposed to work with host elements? As an example, how should If a part attribute is required on the host, then I suppose it’s not possible to style all native elements across all style scopes without them having an explicit part attribute. I suppose that’s the expected behavior, that as a component author I can guarantee that no one can affect the styles of my component unintentionally. I haven’t come to a conclusion with my thoughts whether themable elements should always be exposed in the light DOM so they would participate in global styling without any additional platform capabilities. Somehow it feels like shadow DOM should reserved for internal implementation details, and the component author doesn’t want you to mess with those. |
Using Another complain / concern I have with |
I think the way importance was defined in Shadow DOM (so that the rules in the shadow root always win) were intended to address such a thing (at least for So you could use |
::theme() modifications I've got a concern with the 'old' ::theme proposal why it doesn't work: <html><body>
<style>
::theme(my-button) {
color:hotpink;
}
::theme(my-component1) {
background: red;
}
::theme(my-component2) {
background: blue;
}
::theme(my-component-part) {
background: green;
}
</style>
<my-button part="my-button">text</my-button> <!-- color: inherit ❌ -->
<my-component1 part="my-component1"> <!-- background: inherit ❌ -->
#shadow-root
<div part="my-component-part"></div> <!-- background: green ✅ -->
<my-button part="my-button">text</my-button> <!-- color: hotpink ✅ -->
<my-component2 part="my-component2"> <!-- background: blue ✅ -->
#shadow-root
<div part="my-component-part"></div> <!-- color: green ✅ -->
<my-button part="my-button">text</my-button> <!-- color: hotpink ✅ -->
</my-component2>
</my-component1>
</body></html> But how can we handle those first two crosses? The idea behind In the previous draft I believe it was as follows:
I would propose a change:
This would allow us to fix the above issue. Statefull components How should we handle internal state of a component? How can se select every If we allow to use selectors behind the ::theme(my-button)[disabled] {} This isn't an issue with the Arguments agains the It looks like a hack to 'select' elements though a shadow root? But media queries aren't selectors, they allow for selecting based on the environment and a component isn't an environment variable. So I think we should stay with a selector to select the webcomponent/part. Does your proposal also allow for Style scoping How do we create a different theme area? For example: the content of the page is white and the footer is black, how can we scope the themes, that also work acros shadow boundaries? dark-theme::theme(my-button) { bla:bla; } The bit is the part of on the left? This can't be specified globally, because it needs to be present in every shadowRoot for this to work (think nested themes). This would need a custom webcomponent to achieve this. The idea is that we allow for reading CSS variables in the selector. Those cascade down so we can achieve nested themes. I believe there currently aren't any boolean css properties, but it can still do a string match? ::theme(my-button)::var(--dark-theme) { bla:bla; } |
That should be covered by custom |
Yes, it is. I was mainly looking at a workaround with a syntax that browsers currently parse as valid CSS so that the styles can be accessed through the DOM API, and not resort to parsing strings with regular expressions. Another alternative I played with was
Yeah, that’s a problem for sure, and probably a big enough to discard the idea of using @shadow my-component {
::part(foobar) {
...
}
} |
I'm working on my first "real" app based on lit-element and the styling is giving me a hard time (compared to good old global css). |
It's unlikely anything will be accepted that allows breaking styles in arbitrary shadow roots. For component authors however it's likely they will often want to allow mixing in external styles though. One thought I had was using the extends proposal e.g.: <style>
h1 {
color: pink;
}
</style>
<slide-show>
<title-slide>
<span slot="title">Welcome to Slideshow</span>
<span slot="author">Boris the Spider</span>
<#shadowroot>
<style>
:host {
display: flex;
align-items: center;
justify-items: center;
flex-direction: column;
}
h1 {
/* Makes h1 within the scope to effectively be matched by
selectors that match h1 even if they're outside the
shadowroot
*/
@extends :external(h1);
}
h2 {
@extends :external(h2);
}
</style>
<h1><slot name="title"></slot></h1>
<h2><slot name="author"></slot></h2>
</#shadowroot>
</title-slide>
</slide-show> |
I guess I am just a dumb old copy-paste-oriented developer (but I'm not the only one...). |
For app-specific structural “components”, I’d imagine sharing styles with a mechanism like If we had a scoped custom element registry, you could imagine some kind of a sugar like all components registered with some custom element registry would automatically get the same set of adopted style sheets (not necessarily proposing or endoursing such an idea; just saying that coming up with such a sugar coating is possible). But ultimately, there is a trade off between sharing style sheets across components, and having style isolation between components. At extreme, you end up with all style rules present in every component. That sort of defeats the point of style isolation shadow DOM provides. |
While some people in other teams have concerns about not being able to easily share "normalize" etc between components, the only thing that we do need at Vaadin is an ability to style elements exposed with As an example, even though we allow injecting any CSS into shadow roots of our components, we have an agreement that is documented and should be followed by the teams that use them:
In order to make the discussion more productive, let's focus on theming with shadow DOM isolation in mind. IMO, if a developer wants to "tell the component from the outside to let styles leak in", that is an anti-pattern, same as using
@rniwa that's a good point that I didn't consider previously but actually we have a use case. At Vaadin, we have two themes implementing different designs (our own, called Lumo and Material). Currently they are both implemented with our Having a theming option that works with scoped registries would also help to implement "micro frontends" or "embedding", which is another topic that some of our users are interested in:
It would be nice to come up with a theming option that would support scoped CE registries in a way that all the requirements listed above would be possible without using |
@Jamesernator, I think your example could be solved by letting the user of your Though, if you want to force the styles of the text that users provides and want to respect the global styles, then something else is needed (adoptedStyleSheets, extends, ect). But what if a user slots in content that applies it’s own text styling, like an It might not always be that simple, but as a general principle I think we should encourage placing content nodes, which should be affected by global styles, into the light DOM and not hide them inside shadow DOM. |
I think that’s what we are looking to do here – find a common approach that all component authors should follow, so you wouldn’t need to learn a new way per component. Though, that doesn’t prevent us from creating components with no styling APIs, so in some cases you probably would be unable to adjust styling to suit your needs, without extending or forking the component. |
@rniwa, would a property with functional notation be a good step towards a solution? E.g. except(opacity, border*, background*): unset; (Then |
Maybe. It's hard to say without having a concrete list of use cases. With all these discussions, having a list of concrete use cases was a key to coming up with a sensible solution in web components. |
From my experience working on many projects in the past ~25 years it is a good thing if users of a component (not the author) can decide how they want to use it, even if it has not been intended by the author of the component. |
Please see my above comment #864 (comment) for explanation on this
Regarding the other suggestions:
The existing user-land solutions ( |
Sorry, I meant: "into any webcomponent" |
Speaking about the user-land solutions: we can learn from framework component libraries. Even though they use CSS in JS and not web components, theming concept is mostly the same. So, let me briefly explain this for those who are not in context, and potentially to get more ideas: Theming variablesUse cases
CSS in JS exampleimport { colors } from '@material-ui/core';
const white = '#FFFFFF';
const black = '#000000';
export default {
black,
white,
primary: {
contrastText: white,
dark: colors.indigo[900],
main: colors.indigo[500],
light: colors.indigo[100]
},
} Source: react-material-dashboard SolutionIn the web components world, this theming pattern perfectly maps to custom CSS properties:
Theme overridesUse cases
CSS in JS exampleimport palette from '../palette';
export default {
root: {
color: palette.icon,
'&:hover': {
backgroundColor: 'rgba(0, 0, 0, 0.03)'
}
}
}; Source: react-material-dashboard Solution
I hope now it should be more clear what exactly is missing from the web platform. At Vaadin we are very committed to "make web components a success" and this is one of our pain points. But also it feels to me like we need more input, so I hope everyone tagged by this comment could dedicate a bit of their time to provide any feedback, especially regarding use cases. |
Should work without JSFor me one of the big "selling points" of web components is that web designers (HTML & CSS, no JS skills) have a simple way to just load and use custom tags providing additional features, e.g. calendar, image gallery etc. |
What about something like this:
Just brainstorming, but something along these lines would be nice. |
Let me illustrate our needs, described by #864 (comment), with possible syntax. Use case: "small" presetOur components provide a "small" theme preset, which is distributed as bunch of styles that can be applied by app developers. These styles could look like this (based on real styles): @media (theme-name: vaadin) {
:is(vaadin-text-field, vaadin-select) {
font-size: var(--font-size-s);
}
:is(vaadin-text-field, vaadin-select):state(has-label)::part(label) {
font-size: var(--font-size-xs);
}
} Let's say in the next version, we change label to use @media (theme-name: vaadin) and (theme-version: 2.0) {
:is(vaadin-text-field, vaadin-select):state(has-label)::part(label) {
font-size: var(--font-size-xxs);
}
} Use case: RTL presetAnother real example of such a preset could be a separate file with RTL styles: @media (theme-name: vaadin) {
:is(vaadin-text-field, vaadin-select):dir(rtl)::part(input-field)::after {
transform-origin: 0% 0;
}
} Use case: theme variantsWhile we have concerns regarding @media (theme-name: vaadin) and (hover: hover) {
:is(vaadin-button)::theme(tertiary):not(:state(active)):hover {
opacity: 0.8;
}
} Maybe we could call it Possible APIthis.attachShadow({ mode: 'open', theme: { name: 'vaadin', version: '1.0' }}); Expected benefitsFor library developers
For add-ons developers
For application developers
|
Let's clarify that there are lots of different relationships between component authors and component users where encapsulation concerns are different. In the open-source world - with independent releases and strict semver - it's tempting to say that users should be able to style anything, and if authors change their DOM that it's a breaking, semver-major change. This might be somewhat true, but even in this case many authors will not want to have to release a new major version because of some DOM change that should have been private. Within a single application, it's also tempting to say that the user and author are the same party and encapsulation is only a hinderance. This is also sometimes true, but experience from large applications and teams shows that this doesn't scale. Encapsulation is necessary to avoid an overly fragile codebase that's difficult to change. And at many large companies these days that use a mono repo, encapsulation is even more important. Without releases and versions, any change to a component is a simultaneous to all uses of the component. If users style private details of a component and the component definition changes, then the component author has just unwittingly just broken the user, and often cannot make that change because of tests. This means that users of a component can put unbounded costs on authors and effectively freeze development. Shadow DOM absolutely solves a lot of these problems, and we don't want to eliminate that when addinging theming support. There is a spectrum of use cases, from solo developers to single teams to large orgs, and any solution needs to be usable in these different cases, possibly by dialing back encapsulation. I strongly believe that the author of the component needs to be in control of the level of encapsulation so that they can offer a limited (or not I guess) public interface that they can actually support and maintain. |
@web-padawan FYI
At Google we are trying to figure out how to specifically prevent this approach. This blows encapsulation away completely and is nearly impossible to support. I have to admit to having used this when painted into a corner though. It's powerful, but at a large org, I think we'd prefer if the user actually forked the component so that they're completely on their own and can't prevent forward evolution of the original component. |
That's a valid point. I listed subclassing just to illustrate its problems, and to show how lack of CSS theming API causes some of web components developers to find such workarounds. I agree that we should figure out a working solution that would seamlessly integrate with existing and upcoming CSS features, including |
There's a lot of great suggestions in this thread and ideas for approaching theming. For my part, I can speak to my thought process behind the Theming standardization project. Custom properties can provide context to web components. They can be defined globally but they can also be scoped very narrowly such that they impact only portions of a site. They give web component builders a method of revealing appropriate design hooks without giving away every property to customization. It lets me say, you can set the color but not the width for this border, in order to allow a cohesive look and feel without reducing the design goals of what is being built. By coalescing around a standard naming, we allow users to mix and match web components and with only 1 set of variables, influence them all. |
I should also add that an additional benefit of using custom properties is that you can theme web components as well as vanilla HTML (or frankly, code from any other system) all using the same source of truth. One set of custom properties to rule them all... 😉 |
This is a list of use cases I’ve been going back to over the years, whenever we’ve been thinking about what our styling/theming solutions should support in Vaadin. The use cases try to follow the process which a designer goes through when working on theming an application, from the more generic use cases to the specific ones. I’m marking them with global/local to indicate where these styling customization should be applicable. Global means that the application developer can apply the styles to a component inside any shadow root they need. Local means that the styling only needs to be applicable in a single style scope. 1. Configure the default appearance of all components uniformly (global)Usually the first step when adapting an existing style to a custom brand. Examples what is usually done here:
Advanced use case: be able to override certain styles for all component with a specific style name. 2. Configure the default appearance of a single component (global)After the designer has configured the global theming as far as they can, where all changes still apply to all components, they start working on individual components. For example, let’s say that buttons have a different border radius than any other component (pill shape) and a custom gradient background. The background gradient reacts to the different states of the button (hover, active, disabled). The designer wants those changes to apply for all buttons across the application. Advanced use case: take the styles from a different theme for some components. 3. Configure the appearance of a single component variation (global)After adjusting the default appearance of buttons, the designer wants to adjust the primary button styles. The button component has a built-in style for that (e.g. 4. Create a single component instance variation (local)Now the designer moves to do any one-off, view-specific adjustments needed. The styles they need for this case are not something would make sense to define as a global variant. 5. Create a new uniform variation or all components to work along all the other styles (global/local)The designer wants to create a scoped context where all components adapt to a certain variation. This is close to the first advanced use case (”small variant”). Instead of having an explicit variant for a component (e.g. This could be a one-off situation (local), or something the designer wishes to utilize in multiple places in the app (global). 6. Style a completely new component (global)There is an application-specific component, a switch/toggle with built-in customizable labels for on/off for example, that should adapt to the theme, including to all of the global variants (e.g. small). The designer also wants to define custom variants just for this component, “hide-labels” for example. I can try to provide more context or examples (mockups) if any of this is unclear. |
Let me add a little code context too for those (like me) who mostly think in code: My web component, :host {
--pfe-card--PaddingTop: calc(var(--theme--container-spacer, 16px) * 2);
--pfe-card--PaddingRight: calc(var(--theme--container-spacer, 16px) * 2);
--pfe-card--PaddingBottom: calc(var(--theme--container-spacer, 16px) * 2);
--pfe-card--PaddingLeft: calc(var(--theme--container-spacer, 16px) * 2);
--pfe-card--Padding: var(--pfe-card--PaddingTop) var(--pfe-card--PaddingRight) var(--pfe-card--PaddingBottom) var(--pfe-card--PaddingLeft);
padding: var(--pfe-card--Padding);
} Top, right, bottom, left padding all look to the global Each padding region can be overriden separately using a more scoped approach: pfe-cta {
--pfe-card--PaddingTop: 20px;
} Or by assigning specific classes (or IDs) to those components ( These varying levels of hooks meet several use-cases for large corporate sites such as redhat.com:
References:
|
@justinfagnani |
Use case: styling deeply nested componentsI've been working on a tree webcomponent that is by nature nesting lots of components. To slot the chlidren of a node the tree component has to use shadow DOM. |
I'd like to chime in as someone who's working on shifting towards Web Components (via LitElement) from a jQuery / vanilla JS (not the library) codebase, by transitioning existing elements from jQuery to being Web Components. What appeals to me about Web Components is that they're part of the spec (React, Vue, and Angular all recreate the spec, and I can't deal), meaning they're perfectly interoperable with everything (including jQuery, React, Vue, etc). I can transition something from jQuery to a Custom Element -- let's say I turn what was typically an included template file with global JS hooks into a single Custom Element -- and then interact with that Custom Element with jQuery or vanilla JS. Nothing else has this no-strings-attached support. I can't just turn a single part of a jQuery app into a React/Vue/Angular element and use it in a clean, sane way while keeping everything else jQuery. Anyways, My experience with encapsulation has been a mixed bag. For Leaf (or mostly-Leaf (which I understand is a nonsense term)) elements, as discussed previously, it works pretty well -- the nesting is typically never too serious that in-component fixed styling is fine. But you quickly run into yet another "all-or-nothing" situation where you need to make/style everything in Web Components, or nothing. This is exactly what I dislike about the mega-frameworks that require you adopt their entirely new paradigms. I've seen two major solutions to "I just need my page's styles / FontAwesome [or some other font] / bootstrap / whatever":
I prefer #2, although it feels very dirty. But it works, and it doesn't seem to harm performance, and I can't really think of a reason why I care that much about my own website's app-specific elements being encapsulated -- the client (users) will always be able to bust into them to change whatever they want, so there's no ENFORCEMENT, it's just a "do you want it bad enough?" gap. We talk about "anti-behaviors" like !important but... we have !important. Sure, you shouldn't use it if you're a good webdev, but you have the ability to if you know what you're doing or think that you know what you're doing. GreaseMonkeyI like the users of my website. They're creative. A lot of them have written custom scripts and stylesheets for my website, because it's created in a sane way (it's just HTML, JavaScript, and CSS). People can tinker with it, learn from it, expand on it, and improve my own understanding through the process. Forced encapsulation on Web Components destroys this. You can't write a stylesheet to theme the way a specific button looks anymore, because that button is a sub-component of a sub-component, thirty shadow DOM levels deep, and your sheet can't target it. Websites should not be compiled binaries. Or at least, we should be able to create websites that are not compiled binaries without breaking the spec to do so. I truly believe that the ability to tinker with a website by viewing its source code and making tweaks to the stylesheet or injecting JavaScript is imperative to instilling curiosity and an interest in software engineering in young minds. The more we force encapsulation, the less this is possible. So honestly I'm probably just not going to use Shadow DOM at all for most of my components, and I haven't really seen any compelling reason why that's a bad idea in this context. |
The issue when not using Shadow DOM is that nested / slotted components are not supported, see: |
That is not true? User stylesheets should apply to all elements in all shadow trees. |
This isn't what I've seen, but if you could elaborate I'd love to be wrong. My understanding is that Shadow DOM -- closed or open -- completely encapsulates all CSS except for custom properties and a few predefined properties (such as Are you saying that user style extensions (eg Greasemonkey) are provided a native way to pierce the Shadow DOM, or that they provide their own method to do so through the extension? |
The Polymer team has had some very interesting discussions with our sister Material Components team, which has lead to a bit of a change in perspective on this topic for me. I think there are two broad categories of component authors, wrt to encapsulation and theming:
This last point is really interesting to dive into. For example, in Material Design there are The interface they want is basically: mwc-card {
--material-elevation: 2;
} Not (imaginary expansion to properties, since I don't know the real ones): mwc-card {
--material-box-shadow-length: 8.4;
--material-card-background-dark: rgba(255, 255, 255, 0.25);
/* ... */
} SASS mixins let them do this because they can write mixins that transform logical values into concrete properties and tell users to only style components with the mixins. In building web components using shadow DOM, then team is transforming the mixins to produce sets of CSS custom properties, so that component consumers can use the mixin with logical values, which are expanded to low-level specific values that pierce shadow roots. Something like: mwc-card {
@include elevation(2);
} This will produce the undesirable output with low-level above, but the expectation is that no one writes those properties by hand. This seems like an ok interim approach, because components do not have to modify their own CSS to be themeable. It's not great because components do have to accept many low-level properties, the validity of the properties can't be checked, and the ergonomics are only good if using SASS. The upshot is that the Material and Polymer teams would love to see some movement towards more SASS-like features to enable customization via logical, component-defined properties, as in: mwc-card {
--material-elevation: 2;
} I think this comes down to a number of semi-independent areas worth exploring:
These discussions were very enlightening to me, in that this design system team didn't really want |
@justinfagnani : that's an interesting insight. The two broad category roughly matches what I pointed out somewhere in the past (I can't find it right now):
The issue of mapping a specific range of values / subset of values to a native property is also pretty much a superset of issues I pointed out about filtering the set of properties. So we're in a board agreement in terms of the set of problems at hands. It would be really good if you can somehow compile a set of requirements for these two board categories of components styling use cases (with a concrete use case like a calendar widget with stylable current day indicator). I'd also say that the issue of having to restrict a specific set of allowed values is very akin to the one registered CSS property is trying to solve in Houdini. I'm not necessarily suggesting or endorsing registered property as an API we should have, but we should definitely pay attention to how they tackled this problem on their side. |
Overall the motivation behind the idea described by @justinfagnani seems reasonable to me. It would be great if this could be usable with native elements, too. One of the most requested cases we have at Vaadin (in Lumo design system) is sharing the same set of theme variants between |
@dflorey If the |
just in case this helps anyone out there: you can access CSS variables (custom properties) from within the shadow root and that works fine with stuff like https://github.com/saadeghi/theme-change in light dom |
Back to original concern of this thread: there is a need for IMO it falls not into theming itself but into insulation layers, i.e.
In addition to CSS, Related: DCE Security scopes, |
WCCG had their spring F2F in which this was discussed. You can read the full notes of the discussion (#978 (comment)), heading entitled "Theming / open styling". In the meeting, present members of WCCG reached a consensus to discuss further in breakout sessions. I'd like to call out that #1006 is the tracking issue for that breakout, in which this will likely be discussed further. |
I just realized we didn't have an issue for theming after
::theme
was removed from the Shadow Parts proposal. I'm not sure if this is tracked elsewhere or not.Many, many, web components authors and users need a way to do deep cross-shadow root styling. Shadow parts get us part way there, but require extensive forwarding to enable application-wide or sub-tree theming.
::theme
might solve a lot of cases, but the concept needs to be refined to find an acceptable shape. There are other musing around about more open shadow roots.We've seen a few different userland approaches to theming, often built around injecting styles from a global registry into shadow roots.
I think there are a few variations on the problems to target, and obvious a lot of potential solutions. Hopefully we can gather both here and tease out some commonalities.
The text was updated successfully, but these errors were encountered: