Valid Names for CSS Parts

CSS Shadow Parts, colloquially known as CSS Parts, are used to expose elements inside a web component's shadow root so they can be styled by consumers with CSS. But what are we allowed to call these parts? What characters comprise a valid CSS part name? To find out, I had to dive into the spec!

The part attribute is parsed as a space-separated list of tokens representing the part names of this element.

OK, but what exactly is a "token?" It's not really clear from this page, but we get a hint if we look at the spec for the ::part() pseudo selector:

::part() = ::part( <ident> )

It expects an <ident>, whatever that is. Fortunately, there's a link to it that takes us to a page that defines it.

CSS identifiers, generically denoted by <ident>, consist of a sequence of characters conforming to the <ident-token> grammar.

Perfect! So part names must conform to the <ident-token> grammar, but what's that mean? Let's follow the link to <ident-token> in the spec.

From that page:

<ident-token> [...] have a value composed of zero or more code points. Additionally, hash tokens have a type flag set to either "id" or "unrestricted". The type flag defaults to "unrestricted" if not otherwise set.

That's not, uh, super helpful. There's a link around "code points," so let's see if that makes it more clear. The link goes to this page:

A code point is a Unicode code point and is represented as "U+" followed by four-to-six ASCII upper hex digits, in the range U+0000 to U+10FFFF, inclusive. A code point’s value is its underlying number.

That seems to be a pretty wide range, though. According to Wikipedia:

The last code point in Unicode is the last code point in plane 16, U+10FFFF.

So does this mean we can use any Unicode character as a CSS part name? I'm no Unicode expert, so it still isn't clear.

Experimenting with Various Characters #

Let's experiment and see what actually works in the browser. I created a sample custom element with the following parts, then tried styling them with ::part() to see what actually worked.

✅ part="a"
✅ part="a-b"
✅ part="a_b"
❌ part="a,b"
❌ part="a.b"
❌ part="a$b"
❌ part="a+b"
❌ part="a@b"
❌ part="a?b"

Clearly we can't use any character for part names. The browser doesn't like symbols, which feels intuitive to be honest.

Let's try some more characters.

✅ part="á"
✅ part="ñ"
✅ part="ę"
✅ part="Ф"
✅ part="平"
✅ part="λ"
✅ part="ה"
✅ part="字"

It seems to be OK with non-punctuation. What about emojis?!

✅ part="🥕"
✅ part="🐛"
✅ part="🐓"
✅ part="💩"
✅ part="🤨"

Totally fine. So it seems like any character except punctuation is good. Let's check out that lower Unicode range the spec referred to: U+0000

Here's a document with the lower end character points. It lists a number of the symbols we tried earlier in this range. 🥲

Clearly this range doesn't reflect valid CSS part names. Maybe we overlooked something in the spec, or maybe the CSS spec defines the argument too generically. Surely the HTML spec will explain it better.

Whoops...there's no mention of the part attribute at all. 😭

Let's see if this MDN page can help us clear things up.

A space-separated list of the part names of the element. Part names allows CSS to select and style specific elements in a shadow tree via the ::part pseudo-element.

OK, but what is a valid part name?!

MDN To The Rescue #

I'm still not sure what's correct per the spec. Let's take a look at the ::part() docs in MDN now. I found this, which looks a lot like what we saw in the spec!

::part( `<ident>+` )

There's a link around <ident> that goes to this page, which says:

Screenshot from the aforementioned page that shows the definition of

Thus far, MDN has provided the most human-readable explanation of what a valid ident is, and therefore what a valid CSS part name is. Here's the rundown of valid characters:

This explains why my punctuation examples didn't work. Here's something interesting, though:

an escaped character (preceded by a backslash, )

So can we use any character? Let's try one of those parts with punctuation that didn't work again:

<div part="a.b">

Now let's target it with CSS:

::part(a\.b) {

It works! So the spec is technically correct in that any character can be used, it just wasn't clear in that some of them need to be escaped.

Check this out:

<div part="a.b{c}|d">

And with CSS:

::part(a\.b\{c\}\|d) {

This works too!

Using Parts To Target State #

Until now, I've primary used a single part for each element I wanted to expose. However, per the spec, parts should be used more like classes:

Part names act similarly to classes: multiple elements can have the same part name, and a single element can have multiple part names.

I guess I should change the way I use parts. I used to think of them as an identifier, but thinking of them more like classes as the spec suggests can help fill a gap!

Now that I know underscores are valid, I may explore them as a convention for hover/focus/active states since we never got the ::state selector. For example, we can give parts a synthetic focus state or even "hoist" a natural focus state to another element in the component.

<div part="control control_focus">

With this convention, perhaps we don't need a ::state selector after all!

Just Because You Can, Doesn't Mean You Should #

So there you have it. You can name CSS parts anything you want — just remember to escape special characters! Using the previous example, we can even remove all of the letters and make a part name with only symbols!

<div part=".{}|">

The selector gets a bit crazy, though.

::part(\.\{\}\|) {
  /* 😎 */

I've tested this in all modern browsers and it appears to work consistently. Of course, this isn't very practical. Out of respect for your users, I would advise against naming your CSS parts like this! 🙈

I'm torn on whether or not special characters are useful here. Perhaps for an internal pattern, but I don't think I'd expose it as part of a public API. Users will definitely forget to escape those characters in their stylesheets. For my parts, I'll probably continue using a-z and dashes most of the time.

This post was originally a Twitter thread.