|
| 1 | +<pre class='metadata'> |
| 2 | +Title: CSS Shadow Parts |
| 3 | +Shortname: css-shadow-parts |
| 4 | +Level: 1 |
| 5 | +Group: CSSWG |
| 6 | +Status: ED |
| 7 | +Work Status: exploring |
| 8 | +URL: http://drafts.csswg.org/css-shadow-parts/ |
| 9 | +Editor: Tab Atkins-Bittner, Google, http://xanthir.com/contact/ |
| 10 | +Abstract: This specification defines the ''::part()'' and ''::theme()'' pseudo-elements on <a>shadow hosts</a>, allowing <a>shadow hosts</a> to selectively expose chosen elements from their <a>shadow tree</a> to the outside page for styling purposes. |
| 11 | +Warning: not ready |
| 12 | +</pre> |
| 13 | + |
| 14 | +<pre class=link-defaults> |
| 15 | +spec:selectors-4; |
| 16 | + type:selector; text::hover |
| 17 | + type:dfn; text:dynamic profile |
| 18 | + type:dfn; text:structural pseudo-class |
| 19 | +spec:dom; type:dfn; for:/; text:shadow root |
| 20 | +</pre> |
| 21 | + |
| 22 | +Introduction {#intro} |
| 23 | +===================== |
| 24 | + |
| 25 | +Issue: This spec is intentionally a rough sketch at the moment. |
| 26 | +It should contain all the details necessary to evaluate the proposal, |
| 27 | +but is intentionally avoiding precise algorithms at the moment, |
| 28 | +to aid in easy comprehension |
| 29 | +and to, hopefully, discourage implementation from this sketch. |
| 30 | + |
| 31 | +Shadow DOM allows authors to separate their page into "components", |
| 32 | +subtrees of markup whose details are only relevant to the component itself, |
| 33 | +not the outside page. |
| 34 | +This reduces the chance of a style meant for one part of the page |
| 35 | +accidentally over-applying and making a different part of the page look wrong. |
| 36 | +However, this styling barrier also makes it harder for a page to interact with its components |
| 37 | +when it actually <em>wants</em> to do so. |
| 38 | + |
| 39 | +This specification defines the ''::part()'' and ''::theme()'' pseudo-elements, |
| 40 | +which allow an author to style specific, purposely exposed elements in a <a>shadow tree</a> |
| 41 | +from the outside page's context. |
| 42 | +In combination with <a>custom properties</a>, |
| 43 | +which let the outside page pass particular values |
| 44 | +(such as theme colors) |
| 45 | +into the component for it to do with as it will, |
| 46 | +these pseudo-elements allow components and the outside page |
| 47 | +to interact in safe, powerful ways, |
| 48 | +maintaining encapsulation |
| 49 | +without surrending all control. |
| 50 | + |
| 51 | +Motivation {#motivation} |
| 52 | +------------------------ |
| 53 | + |
| 54 | +For obvious reasons, |
| 55 | +it's valuable to let the outside page style the internals of a shadow tree, |
| 56 | +at least in some limited ways. |
| 57 | +(The ubiquity of UA-specific pseudo-elements for the various input elements shows this.) |
| 58 | + |
| 59 | +The previous proposed method for doing so, |
| 60 | +the >>> combinator, |
| 61 | +turned out to be <em>too powerful</em> for its own good; |
| 62 | +it exposed too much of a component's internal structure to scrutiny, |
| 63 | +defeating some of the encapsulation benefits that using Shadow DOM brings. |
| 64 | +For this, |
| 65 | +and other performance-related reasons, |
| 66 | +the >>> combinator was eventually removed from the <a>dynamic profile</a>. |
| 67 | + |
| 68 | +This left us with using <a>custom properties</a> as the only way to style into a shadow tree: |
| 69 | +the component would advertise that it uses certain <a>custom properties</a> to style its internals, |
| 70 | +and the outer page could then set those properties as it wished on the <a>shadow host</a>, |
| 71 | +letting inheritance push the values down to where they were needed. |
| 72 | +This works very well for many simple theming use-cases. |
| 73 | + |
| 74 | +However, there are some cases where this falls down. |
| 75 | +If a component wishes to allow arbitrary styling of something in its shadow tree, |
| 76 | +the only way to do so is to define hundreds of <a>custom properties</a> |
| 77 | +(one per CSS property they wish to allow control of), |
| 78 | +which is obviously ridiculous |
| 79 | +for both usability and performance reasons. |
| 80 | +The situation is compounded if authors wish to style the component differently |
| 81 | +based on pseudo-classes like '':hover''; |
| 82 | +the component needs to duplicate the <a>custom properties</a> used |
| 83 | +for each pseudo-class |
| 84 | +(and each combination, |
| 85 | +like '':hover:focus'', |
| 86 | +resulting in a combinatorial explosion). |
| 87 | +This makes the usability and performance problems even worse. |
| 88 | + |
| 89 | +We introduce ''::part()'' to handle this case much more elegantly and performantly. |
| 90 | +Rather than bundling everything into <a>custom property</a> names, |
| 91 | +the functionality lives in selectors and style rule syntax, |
| 92 | +like it's meant to. |
| 93 | +This is far more usable for both component authors |
| 94 | +and component users, |
| 95 | +should have much better performance, |
| 96 | +and allows for better encapsulation/API surface. |
| 97 | + |
| 98 | +Another interesting facet of using <a>custom properties</a>, |
| 99 | +however, |
| 100 | +is that inheritance doesn't stop at the first shadow tree. |
| 101 | +Unless explicitly blocked, |
| 102 | +a <a>custom property</a> inherits down thru nested trees, |
| 103 | +allowing authors to style deeply nested components |
| 104 | +as easily as they style directly-visible ones. |
| 105 | +The same considerations apply to this case, |
| 106 | +so we introduce ''::theme()'' to handle this. |
| 107 | + |
| 108 | +It's important to note that ''::part()'' and ''::theme()'' |
| 109 | +offer <em>absolutely zero new theoretical power</em>. |
| 110 | +They are not a rehash of the ''>>>'' combinator, |
| 111 | +they're simply a more convenient and consistent syntax |
| 112 | +for something authors can already do with <a>custom properties</a>. |
| 113 | +By separating out the explicitly "published" parts of an element |
| 114 | +(the <a>shadow part map</a> |
| 115 | +from the sub-parts that it merely happens to contain |
| 116 | +(the <a>shadow theme map</a>, |
| 117 | +it also helps with encapsulation, |
| 118 | +as authors can use ''::part()'' without fear of accidental over-styling. |
| 119 | + |
| 120 | + |
| 121 | +Exposing a Shadow Element: the <{html-global/part}> attribute {#part-attr} |
| 122 | +============================================================= |
| 123 | + |
| 124 | +Any element in a shadow tree can have a <dfn element-attr for=html-global>part</dfn> attribute. |
| 125 | +This is used to expose the element outside the <a>shadow tree</a>, |
| 126 | +and to "forward" sub-parts of the element |
| 127 | +(if it has its own <a>shadow tree</a>) |
| 128 | +to outside the <a>shadow tree</a>. |
| 129 | + |
| 130 | +The part attribute is parsed as a comma-separated list of part mappings. |
| 131 | +Each part mapping is one of: |
| 132 | + |
| 133 | +<dl class=switch> |
| 134 | + : <code>ident</code> |
| 135 | + :: Adds «[ ident → el ]» to the <a>shadow root's</a> <a>shadow part map</a>. |
| 136 | + |
| 137 | + : <code>ident1 => ident2</code> |
| 138 | + :: If el is a <a>shadow host</a>, |
| 139 | + and it's <a>shadow root's</a> <a>shadow part map</a> |partMap| [=map/contains=] ident1, |
| 140 | + then this adds «[ ident2 → |partMap|[ident1] ]» to the <a>shadow root's</a> <a>shadow part map</a>. |
| 141 | + |
| 142 | + : <code>* => prefix*</code> |
| 143 | + :: If el is a <a>shadow host</a>, |
| 144 | + then [=map/for each=] |ident| → |subEl| in el's <a>shadow root's</a> <a>shadow part map</a>, |
| 145 | + «[ prefix + |ident| → |subEl| ]» is added to the <a>shadow root's</a> <a>shadow part map</a>. |
| 146 | + |
| 147 | + : anything else |
| 148 | + :: Ignored for error-recovery / future compat. |
| 149 | +</dl> |
| 150 | + |
| 151 | +Note: It's okay to give a part multiple names, |
| 152 | +or map a sub-part to several names. |
| 153 | +The "part name" should be considered similar to a class, |
| 154 | +not an id or tagname. |
| 155 | + |
| 156 | +Each <a>shadow root</a> has a <dfn export for="shadow root">shadow part map</dfn> |
| 157 | +and a <dfn export for="shadow root">shadow theme map</dfn>, |
| 158 | +both of which are <a>ordered maps</a>. |
| 159 | + |
| 160 | +The <a>shadow part map</a> contains all the entries described by the elements in its <a>shadow tree</a>, |
| 161 | +as described above. |
| 162 | + |
| 163 | +If the <a>shadow root</a>'s {{ShadowRoot/mode}} is {{ShadowRootMode/"closed"}}, |
| 164 | +the <a>shadow theme map</a> is identical to the <a>shadow part map</a>. |
| 165 | +Otherwise, |
| 166 | +it's the concatenation of the <a>shadow part map</a> |
| 167 | +with the <a>shadow theme maps</a> of every <a>shadow host</a>'s <a>shadow root</a> in its <a>shadow tree</a>. |
| 168 | + |
| 169 | +Issue: TODO: Define a syntax or new attribute |
| 170 | +to prevent an element from adding things to its <a>shadow root's</a> <a>shadow theme map</a>. |
| 171 | + |
| 172 | +Selecting a Shadow Element: the ''::part()'' and ''::theme()'' pseudo-elements {#part-theme} |
| 173 | +============================================================================================ |
| 174 | + |
| 175 | +The <dfn selector>::part()</dfn> and <dfn selector>::theme()</dfn> pseudo-elements |
| 176 | +(collectively, the <dfn export>shadow-part pseudo-elements</dfn>) |
| 177 | +allow you to select elements that have been exposed via a <{html-global/part}> attribute. |
| 178 | +The syntaxes of them are: |
| 179 | + |
| 180 | +<pre class=prod> |
| 181 | + ::part() = ::part( <<ident>> ) |
| 182 | + ::theme() = ::theme( <<ident>> ) |
| 183 | +</pre> |
| 184 | + |
| 185 | +The ''::part()'' pseudo-element only matches anything |
| 186 | +when the <a>originating element</a> is a <a>shadow host</a>. |
| 187 | +If the <a>originating element's</a> <a>shadow root's</a> <a>shadow part map</a> |
| 188 | +[=map/contains=] the specified <<ident>>, |
| 189 | +''::part()'' matches the element or elements keyed to that <<ident>>. |
| 190 | +Otherwise, it matches nothing. |
| 191 | + |
| 192 | +<div class="example"> |
| 193 | + For example, |
| 194 | + if you have a custom button |
| 195 | + that contains a "label" element that is exposed for styling |
| 196 | + (via <code>part="label"</code>), |
| 197 | + you can select it with |
| 198 | + ''#the-button::part(label)''. |
| 199 | +</div> |
| 200 | + |
| 201 | +The ''::theme()'' pseudo-element is similar, |
| 202 | +except it can match regardless of whether the <a>originating element</a> |
| 203 | +is a <a>shadow host</a> or not. |
| 204 | +It matches the elements keyed to the specified <<ident>> |
| 205 | +in the <a>shadow theme map</a> of the <a>shadow trees</a> |
| 206 | +of the <a>originating element</a> or any descendants. |
| 207 | + |
| 208 | +<div class="example"> |
| 209 | + For example, |
| 210 | + '':root::theme(label)'' matches any element with <code>part="label"</code> |
| 211 | + anywhere in the entire document, |
| 212 | + no matter how deeply nested into shadow trees they are. |
| 213 | +</div> |
| 214 | + |
| 215 | +The <a>shadow-part pseudo-elements</a> can take additional pseudo-classes after them, |
| 216 | +such as ''x-button::part(label):hover'', |
| 217 | +but never match the <a>structural pseudo-classes</a> |
| 218 | +or any other pseudo-classes that match based on tree information |
| 219 | +rather than local element information. |
| 220 | + |
| 221 | +The <a>shadow-part pseudo-elements</a> also can take additional pseudo-elements after them, |
| 222 | +such as ''x-button::part(label)::before'', |
| 223 | +but never match additional <a>shadow-part pseudo-elements</a>. |
| 224 | + |
| 225 | +<div class=example> |
| 226 | + For example, |
| 227 | + ''x-panel::part(confirm-button)::part(label)'' |
| 228 | + never matches anything. |
| 229 | + This is because doing so would expose more structural information |
| 230 | + than is intended. |
| 231 | + |
| 232 | + One can still target the nested label with a selector like |
| 233 | + ''x-panel::theme(label)''. |
| 234 | + However, this will also select the labels of any other buttons in the panel. |
| 235 | + |
| 236 | + If the <code><x-panel></code>'s internal confirm button had used something like |
| 237 | + <code>part="confirm-button, * => confirm-*"</code> |
| 238 | + to forward the button's internal parts up into the panel's own <a>shadow part map</a>, |
| 239 | + then a selector like |
| 240 | + ''x-panel::part(confirm-label)'' |
| 241 | + would select just the one button's label, |
| 242 | + ignoring any other labels. |
| 243 | +</div> |
0 commit comments