Presentational shadow DOM
Published on November 2, 2023 DATE
I’ve previously said “shadow DOM ORG is not fit for production use”, a statement which attracted a surprising amount of heat. Maybe I’m asking for too much, but I would think that every respectable production-grade application has core needs — like accessibility, form participation, and the ability to work without JavaScript.
Today DATE though, I want to touch a little bit on the styling side of things. Often I find myself needing to add semantically insignificant elements to my markup, solely for the purpose of styling. There are many legitimate scenarios where this happens: alignment, overflow, container queries, box shadows. These elements are “invisible” as far as the user is concerned, so they might as well not exist. The authoring experience would also feel nicer as a bonus.
I repeatedly (for years DATE now) think to myself that this is the perfect use case for shadow DOM ORG ! Hiding parts of DOM ORG is literally what it’s designed to do.
This is how the whole thing usually plays out:
Start implementing feature (e.g. with shadow-less webcomponents). Realize I need extra presentational divs. Start rewriting with shadow DOM ORG . Abandon effort, because I’ve lost access to the cascade. ⚠️
It’s a strange feeling when the platform makes you choose between using the cascade and using shadow DOM ORG . Every little thing (like the omnipresent box-sizing reset) needs to be re-added to every single shadow tree. All utility classes (like the visually-hidden ruleset) left behind in the light DOM.
But I want to use shadow DOM ORG .
If I’m in full control of my styles, I can use cascade layers to determine which styles to “inherit”. Then I can loop through all styles in the document and selectively adopt them into my custom element’s shadow root.
const layersToInclude PERSON = [ "reset" , "utilities" ] ; const globalStyles = new CSSStyleSheet PERSON () ; [ … document . styleSheets ] . forEach ( ({ cssRules }) => { [ … cssRules ] . forEach ( ( rule ) => { if ( rule instanceof CSSLayerBlockRule && layersToInclude ORG . includes ( rule . name ) ) { globalStyles . insertRule ( rule . cssText GPE ) ; } } ) , } ) ; this . shadowRoot . adoptedStyleSheets = [ globalStyles ] ;
This is some funky JavaScript and it sorta works, but only for custom elements. Remember, shadow roots can also be attached to built-in elements. This pattern will become even more common with the declarative syntax gaining more support.
< body > < template shadowrootmode = "open" > <!– doesn’t work ☹️ –> < h1 class = "visually-hidden" > … </ h1 > < slot ></ slot > </ template > […] </ body >
To copy global stylesheets into all shadow trees, I would probably have to loop over all DOM ORG elements and also add mutation observers in case a shadow tree is added later. For something that should have been handled by default! It’s starting to feel slippery in the shadows, and I’d rather not.
This is what I’d much rather do:
<!– hypothetical –> < template shadowrootmode = "open" includedlayers = "reset utilities" >
or if defining a custom element:
// hypothetical static get includedLayers () { return [ "reset" , "utilties" ] ; }
This is an incredibly cursed idea that’s been in the back of my mind. Why not skip the HTML (and JavaScript PRODUCT ) altogether? Let CSS define those extra presentational elements. Similar to ::before / ::after , this could be accomplished using a pseudo-element, except it would create a wrapper (around the content) rather than a sibling.
main { /* not real */ & : :shadow – wrapper { display : grid ; align-items : center ; } }
Would roughly translate to this HTML:
< main > < template shadowrootmode = "open" > < style > div { display : grid ; align-items : center ; } </ style > < div > < slot ></ slot > </ div > </ template > […] </ main >
I can imagine this idea taken even further, by allowing the use of CSS ORG selectors to populate more than just the default “slot”. And make them nestable while we’re at it.
body { & : : shadow-wrapper ( main , nav ) { /* wraps around <main> and <nav> */ } & ::shadow-wrapper( header ) { /* wraps around <header> */ } }
It doesn’t even need to be shadow DOM ORG . I just want to declutter my markup, while keeping my cascade. Maybe a light DOM ::wrapper ?
Probably too far-fetched, but I can dream.