A Modest Web Components Styling Proposal: An “I Know What I’m Doing” Selector

I think the styling story for Shadow DOM Web Components is not great. I’ve got what seems to me like a simple idea that would help that.

Fair warning: I’m just some dude on the internet with an opinion here. I’m coming at it from an author’s perspective who has only written a handful of web components. But I like the idea of them, I like the web platform, and I’ve been trying to watch them for what feels like a long time. I don’t have the wider perspectives of those who have been in the trenches, speccing and implementing these things.

Let me get right to it. What I mean is using a pseudo selector to break through the Shadow DOM and then be able to style inside it, from any CSS on the page:

my-component::i-know-what-im-doing {
  .something-inside {
    /* yay, we're in! */
  }
}Code language: CSS (css)

Now that selector is just tongue-in-cheek (reminds me of React’s dangerouslySetInnerHTML), but the idea I’m serious about. Maybe it’s something more straightforward like:

my-component::root {
  .something-inside {
    /* yay, we're in! */
  }
}

my-component::root footer h3 {
  /* When you're in, select and style like normal CSS */
} Code language: CSS (css)

This is not possible right now, and I wish it were.

Current Styling Options of Shadow DOM Web Components and Why I Don’t Like Them

I’ll elaborate on the thoughts I posted in another article about Web Components. Here are what seem to be the current possibilities for styling a Shadow DOM Web Component:

  • Styling specific elements with ::part is a very niche styling approach that is not a real styling solution. It tosses out the power of CSS (selectors) which seems silly.
  • Styling by documenting somewhere that certain CSS --custom-properties are in use, thus you can set them from the outside, and they will cascade in to affect inner styling. This is also limited, not offering the real power of CSS, and to me not a real styling solution.
  • Styling by injecting a <style> tag into some template literal in the JavaScript itself. This feels awkward and ad hoc to me. Writing CSS as a string is not great (you’d be lucky to get autocomplete, lining, etc.) I’m not a fan.
  • Styling with an adopted stylesheet means an additional web request for each component or back to using a string which is either awkward or slow.

I don’t want to do any of those. They all force me out of the styling workflow of the rest of my website, and tend to push me towards nope, screw it decisions.

The Advantages of This Approach

  • You still get the encapsulation. For the most part, anyway. The JavaScript encapsulation stays the same. Even CSS encapsulation is still there. Random CSS will not leak in. You have to be very specific, making the component name part of the selector and using the specific pseudo-selector to reach into these components. That’s not accidentally done.
  • You can now style things with the best styling API around: CSS.
  • You can concatenate/bundle the styles for your site how you already are doing it. You just… style stuff.
  • You can now choose to create Shadow DOM Web Components, and get the excellent feature of <slot>s, without being penalized with difficult styling choices.
  • All the other styling methods are still there.
  • It would allow people to build and distribute a Shadow DOM Web Component with very basic styling and they could just tell people: hey you wanna style it? Go for it.

OK bye.

16 responses to “A Modest Web Components Styling Proposal: An “I Know What I’m Doing” Selector”

  1. Avatar Chris Coyier says:

    Interesting thought from Nolan:

    Who is opting-in the shadow root to become stylable by its container – the component author or component consumer?

    If it’s the component author, then component consumers may still be frustrated, because not 100% of their third-party components are opted-in. […]

    Whereas if it’s the component consumer, then we have to consider component authors who do not want to be opted-in. It would be similar to allowing component consumers to override a component author’s choice to run in closed mode rather than open mode.

    I guess I’m saying: the component consumer.

    A hardline stance on this would be that I don’t care about component authors that don’t want me to do this. It’s my website. I want control.

    But I could imagine a softer stance where there is some kind of (new) method for force-not-allowing outside styles.

    • Avatar Jeremy says:

      Beyond changing the distributed component code I do not believe web components easily allow mode switching. This is purposeful because the nested children may not be rendered or used in the same way as the plain html shows.

      For example a list component may just have a single ul in the dom but jump across the shadow boundary and that list could be rendered inside a data tables setup with some other magic sauce added. This could work on the regular dom but leveraging the shadow dom allows non-destructive updates. (Changing the list without updating the original or the reverse. Data manipulation that updates the existing dom list)

      Web component styling is still a giant pain for no reason though. There are work arounds but the encapsulation is too aggressive. It causes more pain than what it solves.

  2. Avatar Danny Engelman says:

    Sound like ::nuke is a better name for this selector.

    People who know what it does will use it properly; the other 80% of developers will go WTF? I didn’t hit the red button

  3. Avatar Jason Smith says:

    Piercing the shadow dom with CSS will defeat the idea of shadow root encapsulation. It was designed this way, use ::part

    • Avatar Chris Coyier says:

      Just not a fan of ::part. It’s fake CSS. You can’t do ::part > h3. You can’t do ::part:checked. You have to document what has a part and what doesn’t so any consumer can find it. It’s not a real styling solution.

      • Avatar Niko says:

        Not knowing what does and doesn’t have a part is huge. In React, you can use normal CSS to target anything normally. Why not with Web Components too?

  4. We really need this. I like the idea of scoping it based on the custom element name. It could also not penetrate deeper without further specifying the element-name::root to further penetrate into. Examples:

    some-element::root {
      /* styles in the root of any <some-element> any level deep, but not in roots of sub-custom-elements. */
    }
    
    /* If <some-element>'s root has an <other-element>, we also need to select its root to style inside there. The following styles all other-element root content anywhere: */
    other-element::root { ... }
    
    /* The following styles only other-element shadow root content that is specifically a descendant of a some-element shadow root */
    some-element::root other-element::root {
      ...
    }
    
    /* Same as previous using nested syntax: */
    some-element::root {
      ...
      other-element::root {...}
    }
    
    /* Perhaps some combinators are useful, for example this styles only the other-element root that is an immediate root below the some-element root: */
    some-element::root > other-element::root {...}
    
    /* Same as previous, using nested syntax: */
    some-element::root {
      ...
      & > other-element::root {...}
    }
    

    Etc.

    Regarding the current methods, with the last two options, we don’t have to write a string. We can import from a .css file, making it compatible with CSS tooling:

    // Already works in Chrome and Edge:
    import sheet from './some.css' with { type: 'css' }
    
    shadowRoot.adoptedStyleSheets.push(sheet)
    

    Note that a <link> element with href pointing to a CSS file is another way to add a style without using a string for CSS. shadowRoot.appemd(linkElement).

    Another trick to making elements widely stylable is accepting a stylesheet attribute and equivalent JS property. This is what I’ve done with the <code-mirror> element (for controlling a CodeMirror text editor instance, https://github.com/lume/code-mirror-el) to allow styling its content in any way.

    Here’s the HTML usage:

    <code-mirror stylesheet="
      /* Any styles in here get applied inside the root. */
      .cm-editor {...}
    "></code-mirror>
    

    The JS property also does similar, and accepts various types of input:

    • a CSS string
    • a element
    • a element
    • a CSSStyleSheet instance

    JS usage:

    const link = document.createElement('link')
    link.rel = 'stylesheet'
    link.href = new URL('../relative/path/to/foo.css', import.meta.url)
    
    codeMirrorEl.stylessheet = link
    

    or

    import myStyle from './foo.css' with { type: 'css' }
    codeMirrorEl.stylessheet = myStyle
    

    The implementation of .stylesheet is here:

    https://github.com/lume/code-mirror-el/blob/4bbf21b111a38805107a58fd8fac78a47160eee7/src/CodeMirror.ts#L55-L68

    That JS property receives a string from the HTML attribute, or the differing types of values directly via JS.

    The fully dynamic nature of the attribute/property is here where it simply either appends the style/link element to the shadowRoot, or it pushes to adoptedStyleSheets:

    https://github.com/lume/code-mirror-el/blob/4bbf21b111a38805107a58fd8fac78a47160eee7/src/CodeMirror.ts#L165-L180

    If we change the value of stylesheet, it will update as expected. (It uses Solid.js reactivity underneath, https://solidjs.com)

    Perhaps I can make it also accept an array of the aforementioned types of values, so that multiple styles can be passed in, f.e. from multiple files.

    Another idea is to release that stylesheet feature (maybe renamed to stylesheets if supporting an array) as a standalone mixing usable on any custom elements.

    import {Stylable} from 'name-of-lib'
    class MyEl extends Stylable(LitElement) {
    }
    
    <my-el stylesheets="...">
    

    Maybe another name could be styles instead of stylesheets.

    Lume Element is a library for making custom elements, and is an alternative to other libraries like Lit.

    https://github.com/lume/element

  5. Avatar Um Nontasuwan says:

    There’s a web components styling framework here which can do everything you mention, but in different approach
    https://keenlycode.github.io/adapter/
    I’m the author and would be happy if this one can solve CSS issue for developers 😉

  6. Avatar Michael says:

    I fixed so many awful code during the years, I am sure people who wrote it were all convinced they knew what they were doing

  7. Avatar Kevin Kipp says:

    I 100% get the desire for this, but shadow DOM right now is offering a promise to authors that they can lock users out of styling their web components. I could imagine people being pretty sour if they had built web components using shadow DOM explicitly for this reason and then having that guarantee taken away. Unfortunately, I feel like this would go against the “don’t break the web” ethos — wouldn’t it?

    In any case, hopefully people start using more light DOM with web components anyways, obviating the need for something like this.

  8. So basically the reintroduction of ::shadow or /deep/ selectors.

    These solutions were removed because they felt way too powerful (and possibly performance? I don’t recall exactly..)

    Right now it is designed to give component authors control. My take on that is if a component consumer is desperate enough they will always find a way, it’s what programmers do. I prefer keeping things flexible and it feels like the main thing holding webcomponents back is not being able to style their shadow DOMs. CSS frameworks or just using external global stylesheets (think Tailwind) also just isn’t compatible with the idea and design of shadow DOM, I think Justin Fagnani’s open stylable proposal tackles that use case as well whereas a penetrating shadow selector does not, so I feel like that idea should be considered too.. still opt-in for component authors but perhaps should have a “danger hatch” for consumers to make it open?

Leave a Reply

Your email address will not be published. Required fields are marked *