Presentational shadow DOM

By admin

Presentational
NORP

shadow

DOM
ORG

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.