Identify and Extract Pseudo-Element Selectors from built-in HTML Elements using DevTools

Recently Stefan Judis shared how to style the browse button of a file selector using the ::file-selector-button pseudo-element

But what about other complex elements in the browser? How can we tweak individual parts of those? Take <audio> for example: is there a pseudo-element we can use to style the play button?

~

Table of Contents

  1. Dissecting <input type="file" />
  2. How to use DevTools to peek inside <input type="file" />
  3. Dissecting <audio>
  4. Is there a catch?
  5. One last thing

~

# Dissecting <input type="file" />

Before we answer the question above, let’s first take a look at the <input type="file" /> Stefan used. Below is screenshot of how it’s rendered in Chromium on Mac, with a few extra outlines added.

Even though we only type 1 element in our HTML code, we see it consists of two parts:

  1. A “Choose File” button
  2. A label reading “No file chosen”

Internally, this is also how the browser builds it. We type in:

<input type="file" />

But what’s being rendered is this:

<input type="button" value="Choose file" pseudo="-webkit-file-upload-button" id="file-upload-button">
<span aria-hidden="true">No file chosen</span>

(I’ll show you further down this post how I know this 😉)

The ::file-selector-button selector Stefan mentioned targets only the <input type="button" …> you see there. Using it you can style the button, like he did.

🧐 If you’re paying close attention you might notice that the pseudo-element used is ::file-selector-button, whilst the pseudo attribute of the button reads -webkit-file-upload-button. More on that further down the post 😉

~

# How to use DevTools to peek inside <input type="file" />

In the Chromium DevTools we don’t get to see the two elements that make up <input type="file" />. That’s because this information as is hidden in the Shadow DOM.

Thankfully there is a way to have DevTools show them. To do so, open DevTools’ Settings, and under Elements check the option that reads “Show user agent Shadow DOM”.

🦊 Firefox or 🧭 Safari user? The DevTools in Firefox/Safari have this option enabled out of the box.

Once enabled you’ll see #shadow-root (user-agent) appear in the Elements Tree for all elements that are built that way (and there quite a few!).

<input type="file">
  ↳ #shadow-root (user-agent)
      <input type="button" value="Choose file" pseudo="-webkit-file-upload-button" id="file-upload-button">
      <span aria-hidden="true">No file chosen</span>
</input>

☝️ Having this option enabled all the time can be quite distracting. I personally only turn it on when I need it.

~

# Dissecting <audio>

Winging back to our initial question “is there a pseudo-element we can use to style the play button?”, we can use the DevTools to see the underlying structure.

In Chromium we get back this structure:

<audio controls="" src="/media/cc0-audio/t-rex-roar.mp3">
  ↳ #shadow-root (user-agent)
    <div pseudo="-webkit-media-controls" class="phase-ready state-stopped">
      <div pseudo="-webkit-media-controls-overlay-enclosure">
        <input pseudo="-internal-media-controls-overlay-cast-button" type="button" aria-label="play on remote device" style="display: none;">
      </div>
      <div pseudo="-webkit-media-controls-enclosure">
        <div pseudo="-webkit-media-controls-panel">
          <input type="button" pseudo="-webkit-media-controls-play-button" aria-label="play" class="pause" style="">
          <div aria-label="elapsed time: 0:00" pseudo="-webkit-media-controls-current-time-display" style="">0:00</div>
          <div aria-label="total time: / 0:02" pseudo="-webkit-media-controls-time-remaining-display" style="">/ 0:02</div>
          <input type="range" step="any" pseudo="-webkit-media-controls-timeline" max="2.115918" aria-label="audio time scrubber 0:00 / 0:02" aria-valuetext="elapsed time: 0:00">
          <div pseudo="-webkit-media-controls-volume-control-container" class="closed" style="">
            <div pseudo="-webkit-media-controls-volume-control-hover-background"></div>
            <input type="range" step="any" max="1" aria-valuemax="100" aria-valuemin="0" aria-label="volume" pseudo="-webkit-media-controls-volume-slider" aria-valuenow="100" class="closed" style=""><input type="button" pseudo="-webkit-media-controls-mute-button" aria-label="mute" style="">
          </div>
          <input type="button" pseudo="-webkit-media-controls-fullscreen-button" aria-label="enter full screen" style="display: none;">
          <input type="button" aria-label="show more media controls" title="more options" pseudo="-internal-media-controls-overflow-button" style="">
        </div>
      </div>
      <div role="menu" aria-label="Options" pseudo="-internal-media-controls-text-track-list" style="display: none;"></div>
      <div pseudo="-internal-media-controls-overflow-menu-list" role="menu" class="closed" style="display: none;">
        <label pseudo="-internal-media-controls-overflow-menu-list-item" role="menuitem" tabindex="0" aria-label=" Play " style="display: none;">
          <input type="button" pseudo="-webkit-media-controls-play-button" tabindex="-1" aria-label="play" class="pause" style="display: none;">
          <div aria-hidden="true">
            <span>Play</span>
          </div>
        </label>
        <label pseudo="-internal-media-controls-overflow-menu-list-item" role="menuitem" tabindex="0" aria-label="enter full screen Full screen " style="display: none;">
          <input type="button" pseudo="-webkit-media-controls-fullscreen-button" aria-label="enter full screen" tabindex="-1" style="display: none;">
          <div aria-hidden="true">
            <span>Full screen</span>
          </div>
        </label>
        <label pseudo="-internal-media-controls-overflow-menu-list-item" role="menuitem" tabindex="0" aria-label="download media Download " class="animated-0" style="">
          <input type="button" aria-label="download media" pseudo="-internal-media-controls-download-button" tabindex="-1" style="">
          <div aria-hidden="true">
            <span>Download</span>
          </div>
        </label>
        <label pseudo="-internal-media-controls-overflow-menu-list-item" role="menuitem" tabindex="0" aria-label=" Mute " class="animated-2" style="display: none;">
          <input type="button" pseudo="-webkit-media-controls-mute-button" tabindex="-1" aria-label="mute" style="display: none;">
          <div aria-hidden="true">
            <span>Mute</span>
          </div>
        </label>
        <label pseudo="-internal-media-controls-overflow-menu-list-item" role="menuitem" tabindex="0" aria-label="play on remote device Cast " class="animated-1" style="display: none;">
          <input pseudo="-internal-media-controls-cast-button" type="button" aria-label="play on remote device" tabindex="-1" style="display: none;">
          <div aria-hidden="true">
            <span>Cast</span>
          </div>
        </label>
        <label pseudo="-internal-media-controls-overflow-menu-list-item" role="menuitem" tabindex="0" aria-label="show closed captions menu Captions " class="animated-0" style="display: none;">
          <input aria-label="show closed captions menu" type="button" pseudo="-webkit-media-controls-toggle-closed-captions-button" tabindex="-1" style="display: none;">
          <div aria-hidden="true">
            <span>Captions</span>
          </div>
        </label>
      </div>
    </div>
</audio>

Or visually, with outlines added:

With a bit of digging we can find the the play button on line #9, and extract its pseudo attribute.

<input type="button" pseudo="-webkit-media-controls-play-button" aria-label="play" class="pause" style="">

👉 To style the play button we can use ::-webkit-media-controls-play-button

~

# Is there a catch?

While the ::-webkit-media-controls-play-button selector above works, there’s a catch though: it only works in Chromium based browsers, and this for several reasons:

  1. Every browser engine has its own implementation for what makes up an <audio> element. Shown below is a comparison of the UI for the <audio> as seen in Firefox, Chromium, and Safari.

    Whilst all implementation contain a play button, not all — for example — contain an element that indicates the current time. Styling that wouldn’t be possible.

  2. Not all browser expose the same parts of the UI using pseudo-elements. When it comes to <audio> for example, only Chromium exposes parts of its UI. Firefox and Safari don’t expose any pseudo-element for <audio>

    And even if they would, the wouldn’t use ::-webkit-media-controls-play-button for it.

Another aspect to take into account is that ::-webkit-media-controls-play-button is something that Chromium decided to use. This wasn’t discussed with any other browser vendor. As Thomas Steiner warns:

~

# One last thing

To close off I still owe you an explanation to why Chromium lists ::-webkit-file-upload-button for the browse button of <input type="file" />, instead of ::file-selector-button.

This is because they first exposed it using their own internal ::-webkit-file-upload-button name. It was only later that the CSS Working Group decided to standardize it to ::file-selector-button.

~

Did this help you out? Like what you see?
Thank me with a coffee.

I don't do this for profit but a small one-time donation would surely put a smile on my face. Thanks!

☕️ Buy me a Coffee (€3)

To stay in the loop you can follow @bramus or follow @bramusblog on Twitter.

Three important things you should know about CSS :is()

Back in 2019 I shared how the CSS :is() selector will simplify things when writing CSS. What I didn’t know back then, and only have learnt quite recently, are these three important facts about CSS :is():

  1. The selector list of :is() is forgiving
  2. The specificity of :is() is that of its most specific argument
  3. :is() does not work with pseudo-element selectors (for now)

Let’s take look at what that means.

~

# 1. The selector list of :is() is forgiving

What if you include a selector that’s pure gibberish inside :is()? Will the rule-set be declared invalid or what?

p:is(.foo, #bar, $css:rocks) {
  color: hotpink;
}

Thankfully :is() is very forgiving here: the $css:rocks part — which in itself is an invalid CSS selector — will simply be ignored, while keeping the rest of the selector list in place.. So using the snippet above, both p.foo and p#bar will be colored hotpink. Yay!

Should you try this without :is(), the whole rule-set would become invalid. In the snippet below, none of the paragraphs will be hotpink due to that faulty $css:rocks selector invalidating the whole selector list.

p {
  font-family: sans-serif;
}

p.foo, p#bar, p$css:rocks { /* ❌ This whole rule-set is declared invalid */
  color: hotpink;
}

Note that the paragraphs will have font-family: sans-serif applied, as it’s only the invalid rule-set that ends up being ignored.

🔮 In the near future this latter behavior will no longer be the case as the CSSWG intends to modify these rules such that an invalid selector will simply be ignored rather than invalidating the whole selector list. Relevant CSS WG Issue: 3264

~

# 2. The specificity of :is() is that of its most specific argument

Take the code below. What color will p.foo have?

p:is(.foo, #bar, $this:invalid) {
  color: hotpink;
}

p.foo {
  color: lime;
}

I won’t be lime but hotpink! This because when calculating the specificity, the specificity of the :is() pseudo-class is replaced by the specificity of its most specific argument.

  • p.foo has a specificity of (0,1,1)
  • p:is(.foo, #bar) has a specificity of (1,0,1)

As p:is(.foo, #bar) has a higher specificity, it will “win” here.

☝️ The :not() and :has() pseudo-classes also have their specificity calculated this way.

☝️ If you don’t want to be affected by this, you can use :where() instead of :is(). It works in the same way :is() does, but will always have a specificity of 0. You can cleverly wrap this around other selectors to undo their specificity. Think of :where(:not(…)) for example.

😬 Although I wouldn’t recommend it, you could perfectly do something like :is(#bump#up#the#spe#ci#fi#city#yo, .foo) to override selectors more specific than .foo

~

# 3. :is() does not work with pseudo-element selectors (for now)

If you read up on the definition of :is() you’ll read that it accepts a “Selector List” which is a comma-separated list of simple, compound, or complex selectors.

When looking up simple selectors, there’s an interesting thing to note:

A type selector, universal selector, attribute selector, class selector, ID selector, or pseudo-class is a simple selector.

Do you see it? Here: pseudo-element selectors are not included in this list. As a result, :is() does not play nice with pseudo-element selectors such as ::before, ::after, ….

🔮 In the future this will become possible though, but not just yet. Relevant CSSWG Issue: 2284

~

Knowing these three facts about :is() will surely help you understand it better and make using it more fun!

See the Pen The CSS :is() pseudo-class. What color will .foo have? by Bramus (@bramus) on CodePen.

If you understood well, the Pen above should hold no secrets to you anymore 🙂

~

Did this help you out? Like what you see?
Thank me with a coffee.

I don't do this for profit but a small one-time donation would surely put a smile on my face. Thanks!

☕️ Buy me a Coffee (€3)

To stay in the loop you can follow @bramus or follow @bramusblog on Twitter.

Style Spelling and Grammar Errors with the ::spelling-error and ::grammar-error pseudo-elements

Part of the CSS Pseudo-Elements Level 4 Specification are ways to style spelling and grammar errors.

By default spelling errors — words you have mistyped — get a red dotted underline, thanks to the ::spelling-error pseudo-class selector you can tweak that. Grammar errors — such as a missing capital letter at the beginning of a sentence — can be styled with the ::grammar-error pseudo-class selector.

::spelling-error {
  text-decoration: underline wavy red;
}

::grammar-error {
  text-decoration: underline wavy blue;
}

~

Embedded below is a demo that shows the output of the browser you are visiting with, and the wanted output:


See the Pen The Future of CSS: Style Grammar and Spelling Errors (Demo) by Bramus (@bramus) on CodePen.

As you can see your browser does not yield the proper output, as ::spelling-error/::grammar-error currently have no support in any browser at the time of writing:

~

Once these highlight pseudo-elements do gain browser support, do note that they can only be styled by a limited set of properties that do not affect layout. Only following properties apply to the highlight pseudo-elements:

  • color
  • background-color
  • text-decoration and its associated properties
  • text-shadow
  • stroke-color, fill-color, and stroke-width

~

In case you are interested, here are the relevant bugs to flag/star/follow:

~

🔥 Like what you see? Want to stay in the loop? Here's how:

CSS :nth-of-class selector

One thing that bothers me with CSS :nth-of-type/:nth-child selectors is that you can’t combine them with CSS classes. The selector below for example won’t work as you’d first expect it to work:

.bar:nth-child(2) {
  color: red;
}

No, the selector above won’t color the second .bar element red, something I had somehow expected it to do. What the selector will target instead, is an element that is both .bar and :nth-child(2):

See the Pen .class:nth-of-type by Bramus (@bramus) on CodePen.

🐛 This article previously wrongly assumed that the browser would somehow know that .bar happens to be an article element. That assumption was incorrect and this post has been adjusted to no longer contain this error. Thanks to @simevidas for pointing this out.

~

To solve my problem, I’d be needing something like a :nth-of-class selector. Unfortunately no such selector exists nor is it currently proposed. What is proposed though, in the upcoming CSS Level 4 Selectors specification (aka “not CSS4” 😜), is an extension to the nth-child/nth-last-child pseudo selectors: the ability to add an extra "of S" part into those selectors.

The :nth-child(An+B [of S]?) pseudo-class notation represents elements that are among An+Bth elements from the list composed of their inclusive siblings that match the selector list S. If S is omitted, it defaults to *|*.

Given that, our wanted :nth-of-classselector becomes a reality:

article:nth-child(2 of .bar) {
  color: red;
}

Here’s a pen with the result:

See the Pen .class:nth-of-type by Bramus (@bramus) on CodePen.

If you’re using Firefox or Chrome you’ll see that the demo above is broken, as browser support for it is bad. Only Safari supports it at the time of writing.

💡 Shown above is a dynamic CanIUse.com image, showing an always up-to-date support table. By the time you are reading this browser support might have become better.

Other browser that currently don’t support it have meta-issues to implement CSS Level 4 selectors:

Please vote on those bugs mentioned above if you want those features to land in the browsers.

🗳 By massively voting on browser bugs we can “tell” a browser vendor that we’d like to see a certain feature land. Go forth and vote.

~

Did this help you out? Like what you see?
Thank me with a coffee.

I don't do this for profit but a small one-time donation would surely put a smile on my face. Thanks!

☕️ Buy me a Coffee (€3)

To stay in the loop you can follow @bramus or follow @bramusblog on Twitter.

A Complete Guide to Data Attributes

I like using data attributes in my markup (data-*), CSS ([data-*]), and JS (el.dataset). This post on CSS-Tricks writes down exactly how I use them.

Especially the different type of attribute selectors are really powerful:


// Seven different types of CSS attribute selectors

// This attribute exists on the element
[value]

// This attribute has a specific value of cool
[value='cool'] 

// This attribute value contains the word cool somewhere in it
[value*='cool'] 

// This attribute value contains the word cool in a space-separated list
[value~='cool'] 

// This attribute value starts with the word cool
[value^='cool'] 

// This attribute value starts with cool in a dash-separated list
[value|='cool'] 

// This attribute value ends with the word cool
[value$='cool'] 

The contains selector becomes really powerful when building a JavaScript filter: such a CSS selector can be passed directly into document.querySelectorAll. The yielded result will immediately contain to correct selection, instead of looping over all items that and checking them one-by-one.

A Complete Guide to Data Attributes →

CSS-Only Multiple Choice Quiz

Matthew Somerville:

I followed a link to one of those Guardian end-of-year quizzes on my phone, and had answered a few questions before realising that it was working entirely without JavaScript (I have JavaScript disabled on my phone).

I found this very impressive, well done whoever worked on that, and so I thought I would take a look at how exactly they did it.

The code starts off with a basic set of radio buttons. Using the :checked and :valid attribute selectors (along with some combinators) the right/wrong answers are highlighted once an answer has been given. Changing your answer is also prevented that way. Using CCS Counters – one of CSS’s under-utilised features – the score is kept.

⚠️ Do note that you’re leaking the correctness of answers through your markup this way … so don’t you go making exams with these!

I’ve put all the code together in the pen embedded at the top of this post.

CSS-only multiple choice quizzing →

🔥 Using the same technique, you can also create custom CSS checkboxes or even create interactive emails!

How the CSS :is() selector will simplify things

One of the selectors in CSS Level 4 is :is(). It is the successor to :any() and :matches() (which are supplanted by :is()):

The :is() CSS pseudo-class function takes a selector list as its argument, and selects any element that can be selected by one of the selectors in that list. This is useful for writing large selectors in a more compact form.

/* Without :is */
article > h1,
article > h2,
article > h3,
article > h4,
article > h5 {
  /* … */
}

/* With :is() */
article > :is(h1, h2, h3, h4, h5) {
  /* … */
}

Browser Support isn’t quite there yet though, as they’re either all behind feature flags or use the outdated :matches() or (prefixed) :any()

MDN Web Docs; :is() (:matches(), :any())

Video via @argyleink

The future of CSS: Nesting Selectors

Early March the first draft for the CSS Nesting Module was published. The draft outlines a future mechanism by which we’ll be able to nest CSS selectors natively (e.g. in pure CSS, without the use of any preprocessors)

This module describes support for nesting a style rule within another style rule, allowing the inner rule’s selector to reference the elements matched by the outer rule. This feature allows related styles to be aggregated into a single structure within the CSS document, improving readability and maintainability.

Using the & selector (read this as “the nesting selector”), you can refer to the elements matched by the parent rule:

table.colortable {
  & td {
    text-align: center;
    &.c { text-transform: uppercase }
    &:first-child, &:first-child + td { border: 1px solid black }
  }
  & th {
    text-align: center;
    background: black;
    color: white;
  }
}

As it’s an actual selector, you always need to use the & selector, so you won’t be able to do this:

/* INVALID */
.foo {
  .bar {
    color: hotpink;
  }
}

(The fix would be to write & .bar instead of .bar)

The & selector can also only be used when it’s first compound selector (e.g. it appears at the first position) of an inner selector.

/* INVALID */
.foo {
  .bar & {
    color: hotpink;
  }
}

To cater to these scenarios you’ll need to use the @nest rule, which is more lax:

/* VALID */
.foo {
  @nest .bar & {
    color: hotpink;
  }
}

To me it feels like this @nest rule should go. The interpreter should be smart enough to figure things out all by itself.

CSS Nesting Module (Draft) →