Skip to content
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

Open
justinfagnani opened this issue Jan 22, 2020 · 53 comments
Open

Theming options for shadow roots #864

justinfagnani opened this issue Jan 22, 2020 · 53 comments

Comments

@justinfagnani
Copy link
Contributor

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.

@rniwa
Copy link
Collaborator

rniwa commented Jan 28, 2020

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.

@justinfagnani
Copy link
Contributor Author

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 @apply polyfill an example of userland theming. Details of implementation somewhere deep in here: https://github.com/webcomponents/polyfills/tree/master/packages/shadycss

And @aomarks has been looking at a new system inspired by Vaadin's themable work, @apply, the ::theme idea and CSS shadow parts.

@Jamesernator
Copy link

Jamesernator commented Jan 28, 2020

One of the hazards with ::theme is that it might capture names from shadow roots unintentionally rather than opt-in.

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>

@justinfagnani
Copy link
Contributor Author

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.

::theme previously had been proposed as a new selector that matched elements with part attributes. ::theme was somewhat opt-in in that an element ahd to mark some of its shadow elements as parts for them to be themable, but in the sense that a part attribute didn't necessary opt a grandparent scope into having a styleable part, ::theme wasn't opt-in.

One change discussed was just using a separate attribute, like theme. The idea being that while part shouldn't opt-in to deep theming because it's main intent is to expose a part to the direct containing scope, a theme attribute could be an explicit opt-in to that behavior. Of course the ancestor scopes still haven't opted into deep styling, so the questions become about that. Can theming be opt-out for containing scopes? Shadow roots with themable parts specify those, and they're by default themable from anywhere above in the tree, but containing scopes could have a way to block theming - maybe an option to attachShadow?

@Jamesernator
Copy link

Jamesernator commented Jan 28, 2020

I think lexical names, while useful (the linked proposal is from me), is quite a large lift for this feature.

I don't know how browser stylesheet internals work but it should be no harder than @namespace even including importing them.

The implementation would basically be treat @import './foo.js' { $bar; } as "replace all $bar tokens with https://resolved.url/path/to/foo.js#bar".

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 ::theme(simple-name) to find matching elements, as has been proposed in the past.

@web-padawan
Copy link

web-padawan commented Jan 28, 2020

Regarding the user land solutions: at Vaadin we do inject styles to shadow roots:

While we use part attribute internally, and have an agreement that parts are considered public API following semver, using ::part only isn't enough for cases like "style all the buttons".

Another example of a user-land solution that is currently a prototype is Stylable util:

Let components pick up matching style sheets from the global scope (document, for theming) and from the scope they are contained in (their parent shadow root, for styling).

The matching is done using the standard media query feature. Using a custom media query makes the contained rules (either a full or partial style sheet) inert from the document so that the style rules do not end up matching unintended elements.

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?

@jouni
Copy link

jouni commented Jan 28, 2020

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

@jouni
Copy link

jouni commented Jan 28, 2020

I have some concerns regarding ::theme.

How is it supposed to work with host elements? As an example, how should <vaadin-button> allow itself to be themed? Does it need to add a part attribute on itself in connectedCallback if not already specified by the user? How else could ::theme() target all <vaadin-button> elements in all style scopes?

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. ::part is an escape hatch of sorts, and that might be sufficient if there’s a relatively easy way to attach/adopt style sheets to any shadow root.

@rniwa
Copy link
Collaborator

rniwa commented Jan 29, 2020

Using @ rules is an interesting idea. The one problem I see with that approach is that users of components can defile an arbitrary list of selectors. How does component define which elements' styles can be overridden, and which one can't be?

Another complain / concern I have with ::part and the old ::theme is that there isn't a convenient way to specify a set of properties which are allowed to be changed. It seems like a pretty big oversight to me.

@emilio
Copy link

emilio commented Jan 29, 2020

Another complain / concern I have with ::part and the old ::theme is that there isn't a convenient way to specify a set of properties which are allowed to be changed. It seems like a pretty big oversight to me.

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 ::part, not familiar with the old ::theme spec).

So you could use !important to avoid the outside overriding specific properties, or all: revert !important or such to avoid overriding any properties, and explicitly opt-in after or such.

@paales
Copy link

paales commented Jan 29, 2020

::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 ::theme is that a frontend developer shouldn't care about the rootNode in which the component is in, right?

In the previous draft I believe it was as follows:

  • ::part selectors pierce only 1 shadow boundary.
  • ::theme selectors pierce 1 or more shadow boundaries.

I would propose a change:

  • ::theme selectors pierce 0 or more shadow boundaries.

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 my-button[disabled] and theme that? Should I now also publish all the state attribute of the components as parts with my own naming convention? 🤢

If we allow to use selectors behind the ::theme() bit, this would solve the issue:

::theme(my-button)[disabled] {}

This isn't an issue with the ::part specification because that only pierces one boundary from the outside so you can always select the element directly.

Arguments agains the @media my-component

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 div? How does a component 'whitelist' it's self? How would a component expose certain parts of their inners?

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; }

@web-padawan
Copy link

web-padawan commented Jan 29, 2020

If we allow to use selectors behind the ::theme() bit, this would solve the issue:

That should be covered by custom :state pseudo-class, right? See #738

@jouni
Copy link

jouni commented Jan 29, 2020

It looks like a hack to 'select' elements though a shadow root?

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 @namespace. You can abuse that similarly, that selectors are parsed by the browser but not applied to elements automatically. Then you can traverse the style sheets through the DOM API and pick up the styles you want to apply to your component.


Using @ rules is an interesting idea. The one problem I see with that approach is that users of components can defile an arbitrary list of selectors. How does component define which elements' styles can be overridden, and which one can't be?

Yeah, that’s a problem for sure, and probably a big enough to discard the idea of using @-rules. I assume it’s a bad idea to only parse ::part and :state selectors inside such @-rules? It’s kinda like ::shadow and /deep/ all over again. For example:

@shadow my-component {
  ::part(foobar) {
    ...
  }
}

@dflorey
Copy link

dflorey commented Feb 16, 2020

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).
For what I've learned there is a big difference between small "leaf" components, e.g. a checkbox or a date picker that typically does not contain other components and "node" components like "app-layout", "tab-panel" etc. that cause the most pain when using shadow dom.
I'd prefer not to use shadow dom for the app layout components to be able to use global css styling as much as possible, but unfortunately once you are inside a component that uses shadow dom (e.g. tab panel) you are kind of lost.
For me it would be perfect if I could tell the component from the outside to let styles leak in.
Something like this:
<tab-panel let-css-styles-leak-in="true"><blabla...></tab-panel>

@Jamesernator
Copy link

Jamesernator commented Feb 16, 2020

For me it would be perfect if I could tell the component from the outside to let styles leak in.

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>

@dflorey
Copy link

dflorey commented Feb 16, 2020

I guess I am just a dumb old copy-paste-oriented developer (but I'm not the only one...).
I'd like to get the benefits of webcomponents (=custom tag names, simple component model & packaging, queries in my own shadow dom etc.) but without loosing all the good stuff like global css, icon fonts etc.
When using 3rd party webcomponents I'd just like to use them and tweak the layout by using the inspector and adjusting the styles without the need to learn how theming has been implemented for each lib/component.
So for me a "light" shadow dom that can be enforced when using a component would be ideal.

@rniwa
Copy link
Collaborator

rniwa commented Feb 16, 2020

For app-specific structural “components”, I’d imagine sharing styles with a mechanism like adoptedStyleSheets would work best (there is an on-going discussion about the name & semantics of this particular API).

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.

@web-padawan
Copy link

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 part (and :state when it gets cross-browser support).

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:

Do not rely on the element type which a part applies to. For example, given , you should not rely on the information that the element is actually a native element. This is considered as an internal implementation detail, and the element type could change in the future

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 !important or setting style on elements in shadow tree.

If we had a scoped custom element registry

@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 ThemableMixin approach. Because of that, we can't use them on the same page: component tag names are the same, so the styles would clash.

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:

  • An app is built using "micro frontends" approach and maintained by different teams
  • Independent parts of the app might use different versions of the same web components
  • Web components might have major version bumps that affect public API (part / :state)
  • All the teams want to use consistent theming, but without having to depend on each other when it comes to deployment cycle (some might want to stay on older versions etc)
  • Scoped custom elements registries allow to actually use different major versions in the app

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 <iframe>.

@jouni
Copy link

jouni commented Feb 17, 2020

@Jamesernator, I think your example could be solved by letting the user of your <slide-show> component explicitly slot in <h1 slot="title"> and <h2 slot="author"> elements.

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 <h3 slot="title">? Which styles would you want to be applied? And what about semantics – what happens when you have <h3> inside <h1>?

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.

@jouni
Copy link

jouni commented Feb 17, 2020

@dflorey

When using 3rd party webcomponents I'd just like to use them and tweak the layout by using the inspector and adjusting the styles without the need to learn how theming has been implemented for each lib/component.

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.

@ByteEater-pl
Copy link

@rniwa, would a property with functional notation be a good step towards a solution? E.g.

except(opacity, border*, background*): unset;

(Then all would be equivalent to except().)

@rniwa
Copy link
Collaborator

rniwa commented Feb 26, 2020

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.

@dflorey
Copy link

dflorey commented Feb 26, 2020

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.
That is what has made the web such a success: You could grab an existing component, use it in your project and then find some weird css rules or JS hacks to adjust it to your needs.
In contrast I had to decompile many Java libs to fulfill projects requirements (because methods have been declared private etc.) while the authors would say: This is not how it was intended to use.
I understand the authors point of view , but this is just not how the world works from my experience.
To make web components a success it would be great if there would be a way for all components (not just the ones where the author decided to provide a way to theme the component) to override the styles in every detail.
I think this could be achieved by either let the user decide to let styles from the outside leak into the component, e.g.:
<some-component css="open">
...or by providing a platform mechanism to inject a stylesheet into a webcomponent from the outside (and all nested subcomponents if any).
...or by providing a way to import a css on page level with !important flag that applies to all webcomponents.
...or whatever

@web-padawan
Copy link

let the user decide to let styles from the outside leak into the component

Please see my above comment #864 (comment) for explanation on this

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

Regarding the other suggestions:

inject a stylesheet into a webcomponent from the outside

The existing user-land solutions (Vaadin.ThemableMixin) are doing exactly that.

@dflorey
Copy link

dflorey commented Feb 26, 2020

Sorry, I meant: "into any webcomponent"
The power should be in the hands of the users, not the component authors IMO.
This also mean that it has to be as simple as possible as many web "developers" have good design skills but very limited coding skills (many come from a design background and just have basic HTML and CSS skills).
That being said: I know and love the Vaadin components :-)

@web-padawan
Copy link

web-padawan commented Feb 26, 2020

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 variables

Use cases

  • design tokens: palette colors, size, space, font settings, shadows etc
  • light and dark, high and low contrast presets

CSS in JS example

import { 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

Solution

In the web components world, this theming pattern perfectly maps to custom CSS properties:

Theme overrides

Use cases

  • adjusting components for specific brand / product needs, ideally - using only CSS.
  • theme presets, e.g. in Vaadin we have "compact" theme that needs overrides

CSS in JS example

import palette from '../palette';

export default {
  root: {
    color: palette.icon,
    '&:hover': {
      backgroundColor: 'rgba(0, 0, 0, 0.03)'
    }
  }
};

Source: react-material-dashboard

Solution

  • Extend a custom element base class and override its styles?
    • recommended in Elix blog post by @JanMiksovsky
    • doesn't handle nested elements in shadow DOM
    • workaround: replacing tag names for CSS shadow parts
    • requires to write JS, not available in certain conditions
  • No CSS solution available.

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.

@dflorey
Copy link

dflorey commented Feb 26, 2020

Should work without JS

For 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.
Custom properties work in this context (no JS), but as we know they are very cumbersome to use due to their global nature and really just work well for your "theming variables" usecase. They are not good for fine-tuning components (at least as long as the component developer did not expose every possible css variable for each element as a custom property).
To allow fine-tuning (=adopting an existing component to match the brand in a pixel-perfect way) for web designers we will also need a way to get CSS into any webcomponent without using JS (similar to the old dom-module/template/style approach, but for all webcomponents and simpler if possible)

@dflorey
Copy link

dflorey commented Feb 26, 2020

What about something like this:

<style id="myStyles">
  ...
</style>
<link rel="stylesheet" href="styles.css" id="myLoadedStyles" />
<any-webcomponent adopt-styles="myStyles">
<any-webcomponent adopt-styles="myLoadedStyles">

Just brainstorming, but something along these lines would be nice.

@web-padawan
Copy link

web-padawan commented Feb 26, 2020

Let me illustrate our needs, described by #864 (comment), with possible syntax.

Use case: "small" preset

Our 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 --font-size-xxs. So we add another file:

@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 preset

Another 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 variants

While we have concerns regarding ::theme, here is a real code again that would need it:

@media (theme-name: vaadin) and (hover: hover) {
  :is(vaadin-button)::theme(tertiary):not(:state(active)):hover {
    opacity: 0.8;
  }
}

Maybe we could call it ::variant to have less confusion against old proposal?

Possible API

this.attachShadow({ mode: 'open', theme: { name: 'vaadin', version: '1.0' }}); 

Expected benefits

For library developers

  • expose components to a certain "theme registry" to enable sharing styles
  • create separate CSS files with "theme presets", do not pollute core styles

For add-ons developers

  • create 3rd party components (add-ons) that get styles from a certain theme
  • explicitly depend on specific version of a theme, and upgrade when needed

For application developers

  • using different versions of components with scoped CE registries
  • update individual components to newer versions without rewriting all CSS
  • load (and unload, which is also important) theme presets dynamically

@justinfagnani
Copy link
Contributor Author

@dflorey

Why would you prefer that only publicly exposed pieces can be overwritten?
What if a designer wants to adjust pieces of a component that have not been exposed?

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.

@justinfagnani
Copy link
Contributor Author

justinfagnani commented Feb 26, 2020

@web-padawan FYI

Extend a custom element base class and override its styles?

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.

@web-padawan
Copy link

This blows encapsulation away completely and is nearly impossible to support.

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 ::part() and :state() but not limited to that.

@castastrophe
Copy link

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.

@castastrophe
Copy link

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... 😉

@jouni
Copy link

jouni commented Feb 27, 2020

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:

  • Customize the color palette
    This should be easily doable with custom properties. I don’t see big problems here.
    The same applies to some other theming aspects, like spacing. Basically anything that you usually control with one CSS property.

  • Adjust typography
    Choose at least the font family, size, weight, and line-height for body text, headings, field labels, buttons, etc.
    Custom properties work to a degree, but creating a reusable “style” that includes size, weight, line-height, letter-spacing, etc. is a little complicated (should I create custom properties for all possible font properties per heading level?). In hindsight, the @apply mixins were a nice solution to this, IMO.
    There’s some overlap with this and component-specific styles – should all text types (body, headings, labels, button text) be components, or general purpose, “portable” CSS?

Advanced use case: be able to override certain styles for all component with a specific style name.
For example, create a “small” variant of all components (the same use case @web-padawan demonstated)

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.
For example, there is an existing theme that already defines the desired styling, let’s say the floating labels from Material Design, and the designer would like to use those. Copy-pasting them is an option, sure, but it would be nicer to somehow import those styles instead. Might help to think of a “style module” here.

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. <vaadin-button theme="primary">) and they want to have a different gradient background and font color for that variant (including different states).

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. <vaadin-button theme="small">), all components placed inside this context adapt automatically. An example would be an inverted (dark) color palette for a sidebar/nav in an app.

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.

@castastrophe
Copy link

castastrophe commented Feb 27, 2020

Let me add a little code context too for those (like me) who mostly think in code:

My web component, pfe-card (compiled asset), has the following CSS defined for it's host:

: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 --theme--container-spacer variable first, falling back to a default of 16px if none is defined and then doubles it. The theme variables are all globally scoped variables defined on root.

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 (<pfe-card id="custom">). Doing this completely throws out the reference to container-spacer.

These varying levels of hooks meet several use-cases for large corporate sites such as redhat.com:

  1. I need to change the general spacing across all my web components on my site to be tighter:
    :root {
      --theme--container-spacer: 12px;
    }
  2. I need to change the spacing on all the card components specifically:
    pfe-card {
      --theme--container-spacer: 20px;
    }
  3. I need to change the padding only on all the card components specifically:
    pfe-card {
      --pfe-card--Padding: 20px;
    }
  4. I need to change only the top padding on all the card components specifically:
    pfe-card {
      --pfe-card--PaddingTop: 10px;
    }
  5. I need to change the padding on just these specific cards:
    .custom-card {
      --pfe-card--Padding: 20px;
    }

References:

@dflorey
Copy link

dflorey commented Feb 28, 2020

@justinfagnani
Thanks for taking the time to explain why/when encapsulation of styles is useful. I think it makes perfect sense, especially when working all "leaf" components and in larger orgs.
I just wanted to raise awareness (and I promise this is my last pseudo-philosophical post) that we are about to change the platform with webcomponents and in the long term this may not only affect the way webapps are built, but also how websites will be build (by mashing up webcomponents from different sources). As such we have to be careful not to take away the remaining playground for people that are incompatible with large orgs and still want to make a living from creating web pages by restricting the way things are supposed to be used (blackboxing).
So I still think a master-switch that opens a webcomponent and at the same time voids the warranty of the author or other mechanisms to override the internal styles (e.g. !super-important flag in css) may be a good thing to allow creative use that has not been intended by the author:
https://www.youtube.com/watch?v=jKv_N0IDS2A
Styling should be simple enough for ordinary people (who understand how to use .myclass and #myelement but not how to use .myclass .mynestedclass etc.). Smart people and large orgs have taken almost the entire cake over the past decades, so at least some crumbs should be left for the rest.

@dflorey
Copy link

dflorey commented Feb 28, 2020

Use case: styling deeply nested components

I've been working on a tree webcomponent that is by nature nesting lots of components.
Source:
https://github.com/floreysoft/floreysoft-components/blob/master/packages/floreysoft-tree/Tree.ts
Demo (tree at bottom):
https://floreysoft-components.web.app

To slot the chlidren of a node the tree component has to use shadow DOM.
The user may want render different tree nodes differently, e.g. by adding a .class or #id to some of the nested nodes.
As custom properties propagate to all children, they are not working well in this usecase.
Right now I'm using the style-attribute on the different nodes, but this is of course ugly and very limited.
It would be really nice to be able to apply a stylesheet to the root of the tree with various css selectors to e.g. style all direct children of a certain node.

@geel9
Copy link

geel9 commented Mar 1, 2020

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":

  1. Simply include a <style> element in the shadow root of your element that points to your desired CSS file. If it's already present in the page root, the thought is that this will have minimal impact on performance. I have seen nobody provide evidence for the actual performance impact of this technique.
  2. Don't use Shadow DOM at all. I do this by overriding createRenderRoot() to return this; (typically it creates a shadow root element instead; returning 'this' means that the element's contents are written to itself as its Light DOM children.) Basically, no encapsulation.

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.

GreaseMonkey

I 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.

@dflorey
Copy link

dflorey commented Mar 1, 2020

The issue when not using Shadow DOM is that nested / slotted components are not supported, see:
lit/lit-element#824

@emilio
Copy link

emilio commented Mar 2, 2020

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.

That is not true? User stylesheets should apply to all elements in all shadow trees.

@geel9
Copy link

geel9 commented Mar 4, 2020

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.

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 color and font-size). If any CSS bleeds through Shadow DOM it's because the component author specifically added support through theming or custom properties or some other manual method.

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?

@justinfagnani
Copy link
Contributor Author

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:

  • Those that encapsulation is a bit of a hinderance to. They are asking for ways to open up shadow roots to styling. For LitElement users they are often using our feature that let's them turn off rendering to shadow roots (this often has a downside in that they still want to use <slot>, something clearly not possible). ::part() and something like ::theme() are a potentially solutions to them, for a middle-ground of incrementally breaking encapsulation.
  • Those that encapsulation is important to for maintainability. Material Design and other design systems "at scale" probably fit into this category. Anything that the design system allows to be styled is a public interface that needs to be maintained, at potentially a large cost. ::part may be too broad for these authors, as it allows stylings any property. Interestingly, even individual CSS custom properties that map to specific built-in CSS properties may be too broad, because they allow for any value, some of which may be invalid. These authors want theming and customization to be expressed in terms of their design system's design token (basically variables) abstractions.

This last point is really interesting to dive into. For example, in Material Design there are elevation and density parameters (aka design tokens). They are both enums that get exposed as a number of CSS properties applied to a number of implementation elements within various component's shadow roots. elevation may effect the box-shadow length of some elements, but the authors do not want to allow users to directly and arbitrarily set the box-shadow.

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:

  1. Expressiveness: Transforming logical values into concrete properties requires more powerful calculations that CSS has. Conditionals and boolean operations are very important. Maybe some sort of look up system. Element queries have a number of important uses too.
  2. Abstraction: Mixins of some sort seem like they solve the code-sharing problem very well. They are important for component authors to share the functions that transform logical values into concrete values. Examples seem to utilize mixins to produce single property values, sets of name/value pairs, and sets of rules with selectors.
  3. Modularity: For CSS code sharing, developers want to be able to import mixin and variable definitions. I think something like the CSS References proposal could address this.
  4. Extensibility: There is some desire to do styling work in JS. Style observers and/or custom CSS functions in JS would be useful here. I realize this is quite a big lift though.

These discussions were very enlightening to me, in that this design system team didn't really want ::theme(). I think there are still other good use cases for ::theme(), especially with "white label" components that want to allow many things to be styled arbitrarily (like ING's Lion Components), and for "standalone" like video players and such.

@rniwa
Copy link
Collaborator

rniwa commented Mar 21, 2020

@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):

  • Reusable component that needs to have strict control over the stability of each element in its implementation
  • Structural application specific component that needs a lot of flexibility in its styling

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.

@web-padawan
Copy link

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 <a> and <vaadin-button> elements for consistency - see https://github.com/vaadin/vaadin-lumo-styles/issues/5

@trusktr
Copy link
Contributor

trusktr commented Apr 3, 2021

@dflorey If the /deep/ selector came back to life, would that solve the issues with the unwanted encapsulation, allowing the escape hatch to style anything any level deep? Was there a particular issue that we would want to not bring back from the /deep/? Or big orgs simply didn't want users styling their internals?

@Sleepful
Copy link

Sleepful commented Dec 23, 2022

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

@sashafirsov
Copy link

sashafirsov commented Dec 23, 2022

Back to original concern of this thread: there is a need for common cross-shadow-root theming.

IMO it falls not into theming itself but into insulation layers, i.e. scope. The scope

  • would have common css rules( theme )
  • can inherit another scope
  • there are can be multiple scopes( a library concept )

In addition to CSS, scope can have a common custom element registry, JS insulation, etc.

Related: DCE Security scopes,
microapplication container scope

@keithamus
Copy link
Collaborator

keithamus commented Apr 21, 2023

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

16 participants