The following is an extract from our book, CSS Master, written by Tiffany B. Brown. Copies are sold in stores worldwide, or you can buy it in ebook form here.
As mentioned earlier in this chapter, pseudo-classes help us define styles for documents based on information that is unable to be gleaned from the document tree or can’t be targeted using simple CSS selectors. These include logical and linguistic pseudo-classes such as :not()
and :lang()
. It also includes user-triggered pseudo-classes such as :hover
and :focus
.
In this section, we’ll cover some esoteric and lesser-known pseudo-classes with a focus on what is available in browsers: child-indexed and typed child-indexed pseudo-classes, and input pseudo-classes. Child-indexed and typed child-indexed pseudo-classes let us select elements by their position in the document subtree. Input pseudo-classes target form fields based on their input values and states.
Highlighting Page Fragments with :target
A fragment identifier is the part of a URL that follows the #
; for example, #top
or #footnote1
. You’ve probably used them to create in-page navigation: a so-called “jump link.” With the :target
pseudo-class, we can highlight the portion of the document that corresponds to that fragment, and we can do it without JavaScript.
Say, for example, that you have series of comments or a discussion board thread:
<section id="comments">
<h2>Comments on this post</h2>
<article class="comment" id="comment-1146937891">...</article>
<article class="comment" id="comment-1146937892">...</article>
<article class="comment" id="comment-1146937893">...</article>
</section>
With some CSS and other fancy bits, it looks a little like what you see in the figure below.
Each comment in the aforementioned code has a fragment identifier, which means we can link directly to it. For example, <a href="#comment-1146937891">
. Then all we need to do is specify a style for this comment using the :target
pseudo-class:
.comment:target {
background: #ffeb3b;
border-color: #ffc107
}
When the fragment identifier part of the URL matches that of a comment (for example, http://example.com/post/#comment-1146937891
), that comment will have a yellow background, seen below.
You can use any combination of CSS with :target
, which opens up some fun possibilities such as JavaScript-less tabs. Craig Buckler details this technique in his tutorial “How to Create a CSS3-only Tab Control Using the :target
Selector.” We’ll update it a bit to use more CSS3 features. First, let’s look at our HTML:
<div class="tabbed-widget">
<div class="tab-wrap">
<a href="#tab1">Tab 1</a>
<a href="#tab2">Tab 2</a>
<a href="#tab3">Tab 3</a>
</div>
<ul class="tab-body">
<li id="tab1">
<p>This is tab 1.</p>
</li>
<li id="tab2">
<p>This is tab 2</p>
</li>
<li id="tab3">
<p>This is tab 3.</p>
</li>
</ul>
</div>
It’s fairly straightforward, consisting of tabs and associated tab content. Let’s add some CSS:
[id^=tab] {
position: absolute;
}
[id^=tab]:first-child {
z-index: 1;
}
[id^=tab]:target {
z-index: 2;
}
Here’s where the magic happens. First, we’ve absolutely positioned all of our tabs. Next, we’ve made our first tab the topmost layer by adding z-index: 1
. This is only important if you want the first tab in the source order to be the first tab users see. Lastly, we’ve added z-index: 1
to our target tab. This ensures that the targeted layer will always be the topmost one. You can see the result below.
Tip: Improving Accessibility
A more accessible version might also use JavaScript to toggle the hidden
or aria-hidden=true
attributes based on the visibility of each tab body.
Clicking a tab updates the URL with the new document fragment identifier. This in turn, triggers the :target
state.
Negating Selectors with :not()
Perhaps the most powerful of this new crop of pseudo-classes is :not()
. It returns all elements except for those that match the selector argument. For example, p:not(.message)
selects every p
element that lacks a message
class.
The :not()
pseudo-class is what’s known as a functional pseudo–class. It accepts a single argument, much like functions in other programming languages do. Any argument passed to :not()
must be a simple selector such as an element type, a class name, an ID, or another pseudo-class. Pseudo-elements will fail, as will compound selectors such as label.checkbox
or complex selectors such as p img
.
Here’s an example of a form that uses textual input types and radio buttons:
<form method="post" action="#">
<h1>Join the Cool Kids Club</h1>
<p>
<label for="name">Name:</label>
<input type="text" id="name" name="name" required>
</p>
<p>
<label for="email">Email:</label>
<input type="email" id="email" name="email" required>
</p>
<fieldset>
<legend>Receive a digest?</legend>
<p>
<input type="radio" id="daily" name="digest">
<label for="daily" class="label-radio">Daily</label>
<input type="radio" id="weekly" name="digest">
<label for="weekly" class="label-radio">Weekly</label>
</p>
</fieldset>
<button type="submit">Buy Tickets!</button>
</form>
In the HTML, labels associated with a radio
type have a .label-radio
class. We can use the :not()
pseudo-class: to target those elements without a label-radio
class, as shown in in the figure below:
label:not(.label-radio) {
font-weight: bold;
display:block;
}
Here’s a trickier example. Let’s create styles for textual inputs. These include input types such as number
, email
, and text
along with password
and url
. But let’s do this by excluding radio button, check box, and range inputs. Your first instinct might be to use the following selector list:
([type=radio]),
input:not([type=checkbox]),
input:not([type=range]) {
...
}
Unfortunately, this won’t work, as each selector overrides the previous one. It’s the equivalent of typing:
input:not([type=radio]){ ... }
input:not([type=checkbox]) { ... }
input:not([type=range]) {... }
Instead, we need to chain our :not()
pseudo-classes, so that they all filter the input
element:[4]
input:not([type=radio]):not([type=checkbox]):not([type=range]) {
...
}
Using pseudo-classes (and pseudo-elements) without a simple selector is the equivalent of using it with the universal selector. In other words, :not([type=radio])
is the same as *:not([type=radio])
. In this case, every element that lacks a type
attribute and value of radio
will match―including html
and body
. To prevent this, use :not()
with a selector such as a class name, ID, or attribute selector. By the way, this also holds true true for class name, ID, and attribute selectors: .warning
and [type=radio]
are the same as *.warning
and *[type=radio]
.
CSS Selectors Level 4 refines the way :not()
works, so that it can accept a list as an argument, and not just simple selectors. Rather than chaining pseudo-classes as previously, we’ll be able to use a comma-separated argument:
input:not([type=radio], [type=checkbox], [type=range]) {
...
}
Unfortunately, no major browser supports this yet, so use chaining in the meantime.
[4] The selector chain below will also match [type=image]
, [type=reset]
, [type=color]
, and [type=submit]
elements.