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

[css-cascade][css-scoping] Host layerable shadow roots #9792

Open
knowler opened this issue Jan 13, 2024 · 4 comments
Open

[css-cascade][css-scoping] Host layerable shadow roots #9792

knowler opened this issue Jan 13, 2024 · 4 comments

Comments

@knowler
Copy link

knowler commented Jan 13, 2024

I originally proposed this here: WICG/webcomponents#909 (comment). I talked with @mirisuzanne and she recommended we continue to flesh out this proposal here before bringing it back to WICG. What follows is more so geared towards one who has read/understands the CSS Cascading and Inheritance spec (see the original proposal for a more web-components-oriented perspective).

This is a proposal to allow cascade layers within an outer context (e.g. a document or shadow root) to style elements within an inner context (i.e. elements within a shadow root).

In regard to the Cascade, this would effectively bump the corresponding shadow context down in the sorting order to be within the outer context’s layers (i.e. sorted after whatever layer it was marked for access).

For authors of the outer context (e.g. a consumer of a Web Component using the Shadow DOM), this would mean greater styling flexibility than what is available through existing shadow root styling APIs (e.g. CSS Parts, slots, custom properties).

For authors of the inner context (e.g. an author of a Web Component using the Shadow DOM), this would mean reduced style encapsulation—but style encapsulation nonetheless:

  • Since the inner context’s styles would be cascaded after the permitted outer context’s layers, the inner context’s styles would take precedence unless the those layers were using !important.
    • This might seem a bit strange, but I think that it forces the consumer to understand that what they are overriding is an important default for the subsequent levels in the cascade. If the component author has set an element with “important defaults” as a CSS Part that might be a more appropriate API for the consumer to use to style said element where it’s actually used.
  • The inner context can use all: revert to effectively ignore styles from the host’s permitted layers for an element.
    • Again, this could be circumvented by the author of the outer context using !important.
  • The inner context would still remain isolated from subsequent layers (including unlayered styles) which would mean its own styling APIs using CSS Parts, slots, and custom properties would still be effective.
  • Allowing a component consumer to set some default styles can be advantageous for a component author:
    • Slots can be burdensome for their consumers and lose the benefits of DOM encapsulation for the component author.
    • CSS Parts might be too limited for a consumer and awkward to work with, especially with an existing system.

How this would work?

TODO: add better examples here

Here’s an idea for the CSS syntax (shadow() might not be great since drop-shadow() is a thing):

/* host's context (e.g. a document's stylesheet */
@layer defaults shadow(my-element), components, layouts;

@layer defaults {
  /* Anything here will apply to elements in `my-element`’s shadow tree */

  button {
    background-color: DeepPink;
  }
}

@layer components {
  /* Subsequent layers will not apply to the shadow tree’s elements */
}

@layer layouts {
  /* Subsequent layers will not apply to the shadow tree’s elements */
}

/* Unlayered styles (i.e. a subsequent layer) will not apply to the shadow tree’s elements */

In this case, the simplified expected sorting order (for elements in the shadow root):

  1. User agent styles
  2. User styles
  3. The host context’s defaults layer.
  4. The shadow context
  5. The rest of the host context’s styles starting with the layouts layer for the exposed elements (e.g. CSS Parts).

In cases where the shadow root was layered after the first layer, it would still receive the styles of the previous layer(s):

/* host's context (e.g. a document's stylesheet */
@layer defaults, components shadow(my-element), layouts;

@layer defaults {
  button { /* These will apply to `button` elements in `my-element`’s shadow tree */ }
}

@layer components {
  button { /* These will apply to `button` elements in `my-element`’s shadow tree */ }
}

@layer layouts {
  .some-layout button {
    /* These will not apply */
  }
}

For this case, the simplified expected sorting order (for elements in the shadow root):

  1. User agent styles
  2. User styles
  3. The host context’s defaults and components layer.
  4. The shadow context
  5. The rest of the host context’s styles starting with the layouts layer for the exposed elements (e.g. CSS Parts).

Use cases

TODO: add more use cases

  • It allows a component consumer greater styling control over a component than what is currently available through CSS Parts.
    • TODO: explain why CSS Parts is not a great API for this kind of styling (it’s good for other styling though).
  • It allows a component author a way to allow elements in their tree to receive “defaults” from the document while still preserving the component’s own styling API (whether that be using parts or custom properties).
  • It can allow legacy CSS design systems to apply to new custom elements that are using the Shadow DOM for templating.

Problems with this proposal

  • Priority does not translate to access. Using layers would be using them to do something beyond their design.
  • Cascading before doesn’t necessarily solve all potential use cases: more granularity might be required (i.e. being able to “slot styles” into different points within a shadow root’s cascade layers).
  • Reset styles tend to be far reaching and so applying a layer and all of its prior layers could have potentially ill effects.
    • This wouldn’t necessarily be an unsurmountable problem, but it could turn out to be a footgun.
@matthewp
Copy link

@knowler are your example stylesheets that are inside the shadow DOM or outside?

@knowler
Copy link
Author

knowler commented Jan 13, 2024

@knowler are your example stylesheets that are inside the shadow DOM or outside?

@matthewp That's outside. I'll update it to make that clear. I plan on making the example also include some shadow context styles to further explain how it will work.

@mirisuzanne
Copy link
Contributor

mirisuzanne commented Jan 13, 2024

There seem to be two issues at play: styling access to the shadow DOM and styling priority in relation to shadow DOM styles. Layers are well designed for helping with the priority side. I like the idea of layering custom element styles into a page style, much the same way I would layer-in styles from other third-party tools. That's what layers were designed for!

I'm a bit less convinced by the way layers are used here to also grant access. For one thing, it feels a bit tangential to the layering, and not entirely obvious that adjusting the priority also provides access for some but not all page styles – based on the priority of those styles. I'm not sure it makes sense to base access on priority like that. But also: this puts a limit on the ability of layers to do what they're designed for: explicitly re-arrange priorities.

I can see use-cases where that works, but also use-cases for the opposite. When I ship a web component, I want to provide default styles that are easy to override. The current priority order is fine, with the page taking precedence. What I really want to do is provide access for those page styles to get in.

@knowler
Copy link
Author

knowler commented Jan 14, 2024

@mirisuzanne I do think that access goes hand-in-hand with priority though (inversely, i.e. access decreases in subsequent layers). It might not be what Cascade Layers were explicitly designed for, but just how Cascade Layers came to be out of an eye towards how the Cascade already worked, that’s what inspired how this feature should work. User agent and user styles have access to shadow contexts for styling, so shifting the shadow context down to be within the layers of the document styles, the layers which come before would have access in the same way. Just because access seems to be tied to priority in this way, doesn’t mean that’s how Cascade Layers should work, but I think it does work well with the design of shadow DOM styling APIs.

My goal with this feature isn’t to replace those APIs (i.e. custom elements, CSS Parts, custom properties, :state() via CustomStateSet). I think they are still the right tool for the problems that they solve: providing a more declarative styling API for an HTML component abstraction that becomes more maintainable for the consumer. If anything, I hope that this feature will actually free them up to do what they were designed to do. I fear by opening access without keeping that tied to a level of priority will effectively make these existing APIs useless. Sure if developers wanted to, they could use a feature like this to style all of their shadow roots, but I think they’d lose out on the power of the existing Shadow DOM styling APIs since style encapsulation is what makes them useful.

I think maybe the underlying challenge here is that theming requires coordination across layers in the cascade. Theming often requires some access to defaults, since it’s not easy for component authors to be able to make their component inherently/universally themable, especially as a component becomes more complex (i.e. composing elements). I actually think that might be what this experimental “Defaults Styles for Custom Elements” section of the latest Scoping Level 1 Editor’s Draft is trying to get at.

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

No branches or pull requests

4 participants