One of the most important principles in software engineering is known as the separation of concerns. You shouldn’t mix, say, database queries and tax calculations in the same function, for a whole raft of reasons – not least that you shouldn’t have to connect to a database to be able to test that your code calculates taxes correctly.
As we talked about earlier in the course, separation of concerns in frontend web development is about separating content, presentation, and behaviour. HTML should focus on the structure of the content - what does it mean? What are the elements that make up a page, and what are the relationships between those elements? CSS then gives us the tools to interrogate that structure and apply colours, fonts, borders, and other visual elements to make that structure more apparent to humans. Got a bunch of links that belong together? Use HTML to group them using a list or a menu element, then use the CSS to draw a border around it.
The key to doing this is being able to target the elements you want to style — and, in some cases, dynamically add new elements to the page during rendering. We’ve already learned about CSS selectors based on elements, class names, and element IDs; let’s meet the rest of the CSS selector family, and their cousins, the pseudo-classes and pseudo-elements.
First, let’s remind ourselves what we’re trying to accomplish here.
Here’s some bad HTML:
<!DOCTYPE html>
<html>
<head>
<title>Bad HTML</title>
<style>
section.red {
color: red;
}
h1.green {
color: green;
}
h1.large {
font-size: 400%;
}
p.italic {
font-style: italic;
}
p.navy-blue {
color: navy;
}
p.bottom-border {
padding-bottom: 1em;
border-bottom: 2px solid purple;
}
</style>
</head>
<body>
<section class="red">
<h1 class="green large">The first H1 in a section should be big, and green.
</h1>
<p class="italic navy-blue">The first paragraph after an H1 should be navy
blue, and in italics.</p>
<p>This a normal paragraph.</p>
<p>This a normal paragraph.</p>
<p class="bottom-border">The last paragraph in a series of paragraphs should
have a purple bottom border.</p>
<h1>Other H1 tags should NOT be green or super-large.</h1>
<p>This a normal paragraph.</p>
<p>This a normal paragraph.</p>
</section>
</body>
</html>It’s not wrong - there aren’t any syntax errors or invalid tags - but I don’t like it at all. Class names are based on what we want the element to look like, not on what it means. Imagine a ticket comes in asking you to change all the green headings to be purple… you could change the name of the rule, and then go trawling through the code doing a global search & replace, replace green with purple, but it’s easier — and safer — to just rewrite the rule…
h1.green {
color: purple;
}
…and now you’ve got a rule on your website that says all the green headings should be purple. Great work.
Compare that to this:
<!DOCTYPE html>
<html>
<head>
<title>Bad HTML</title>
<style>
section.promotion {
color: red;
}
h1.first-in-section {
color: green;
font-size: 400%;
}
p.immediately-after-h1 {
font-style: italic;
color: navy;
}
p.last-in-series {
padding-bottom: 1em;
border-bottom: 2px solid purple;
}
</style>
</head>
<body>
<section class="promotion">
<h1 class="first-in-section">The first H1 in a section should be big, and
green.
</h1>
<p class="immediately-after-h1">The first paragraph after an H1 should be
navy
blue, and in italics.</p>
<p>This a normal paragraph.</p>
<p>This a normal paragraph.</p>
<p class="last-in-series">The last paragraph in a series of paragraphs
should
have a purple bottom border.</p>
<h1>Other H1 tags should NOT be green or super-large.</h1>
<p>This a normal paragraph.</p>
<p>This a normal paragraph.</p>
</section>
</body>
</html>The class names here aren’t based on what the element should look like, they’re based on where that element appears in the structure - and then the CSS uses that information to style them according to our requirements.
But, other than the section being given the promotion class, most of the class names here aren’t actually communicating anything that we can’t infer from the document structure itself. The browser can’t tell that this page is some sort of product promotion unless we add a class name, but it can absolutely tell that this h1 is the first element in a section, that there’s a series of paragraphs immediately after it, and so on.
Using modern CSS, we can target elements based on their relationships without having to use extraneous class names. Let’s find out how.
Combinators
We’ve already met the descendant combinator, which is just a space between two selectors:
p a { }- match anyaelement which is a descendant of apelement.main .highlight { }- matches any element with ahighlightclass which is a descendant of amainelement.promo #call-to-action- matches the element with the IDcall-to-action, but only when that element is a descendant of an element with apromoclass.
There are four more combinators you should know about.
- Child combinator, indicated by a greater-than character
>.p > amatches anyaelement which is a direct child of apelement - Subsequent sibling combinator indicated by a tilde
~.h3 ~ pmatches every paragraph that occurs anywhere following anh3element, as long as they share a parent. - Next-sibling combinator indicated by a plus
+.h3 + pmatches a singlepwhich is immediately preceded by anh3, and shares the same parent.
CSS also supports the column combinator
||, which we’ll talk about in the section about styling tables and tabular data.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Combinators</title>
<style>
div,
p {
padding: 0.5em;
border: 1px solid white;
margin: 0.5em;
}
article div {
background-color: crimson;
}
article>div {
background-color: royalblue;
}
article p {
background-color: gold;
}
div+div {
background-color: magenta;
}
p~div {
background-color: indigo;
color: white;
}
p+div {
background-color: green;
color: white;
}
</style>
</head>
<body>
<article>
<div>
I'm a div. My parent is an article. I should be blue.
<div>I'm a div. My grandparent is an article, but my parent is not, so I
should be crimson.</div>
</div>
<div>I'm a div. My older sibling is a div. I should be magenta.</div>
<p>I'm a paragraph. My parent is an article. I should be gold.</p>
<div>I'm a div. My adjacent sibling is a paragraph. I should be green.</div>
<div>I'm a div. My older sibling is a paragraph. I should be indigo.</div>
</article>
</body>
</html>Location Pseudo-classes
Web pages are interactive. Users click on links, fill out forms — even just moving the mouse around the screen can be a form of interaction.
Now, in theory, this is all nice & simple. Everything’s a :link until something happens. If you move the focus to that element - e.g. using the Tab key on your keyboard - it gets the :focus pseudo-class. When you hover over it, it gets the :hover pseudo-class; when you’re actually clicking it, it gets the :active pseudo-class, and then any link to a page you’ve visited before gets the :visited pseudo-class.
In practice… it’s way more complicated than that. Hover states work great on devices with a mouse pointer, but on touchscreen devices like smartphone and tablets, you can’t hover or focus an element - you’re either touching it, or you’re not.
In this example, each side of the border responds to a different pseudo-class, and the background-colour responds to the :active state:
<!DOCTYPE html>
<html lang="en">
<head>
<title>State Selectors</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
nav {
margin: 1em;
}
a,
input {
display: block;
width: 100%;
box-sizing: border-box;
margin: 1em;
text-decoration: none;
border: 20px solid silver;
padding: 1em;
}
a:link {
border-left-color: crimson;
}
a:visited {
border-right-color: purple;
}
a,
input {
&:hover {
border-bottom-color: royalblue;
}
&:active {
background-color: gold;
}
&:focus {
border-top-color: forestgreen;
outline: none;
}
}
</style>
</head>
<body>
<nav>
<a href="https://google.com/">Google</a>
<a href="https://kagi.com/">Kagi</a>
<a href="https://bing.com/">Bing</a>
<a href="https://yahoo.com/">Yahoo!</a>
<input placeholder="I’m an input field - you can focus me on a smartphone">
</nav>
</body>
</html>If we tab around using the keyboard, you’ll see the focus move to each link in turn; if we move the mouse over an element, we get the hover pseudo-class; if we click on it, it goes active, and then when we come back to the page, it doesn’t have the :link pseudo-class any more, it has the :visited pseudo-class.
Now, let’s open that same page on a smartphone. You’ll notice that links on a mobile device never get the focus, although we can focus the <input> element; elements will briefly get the active state when we click them, but because by that point we’re already navigating away, there’s not a whole lot you can do with it.
There are a whole bunch more pseudo-classes which only apply to inputs and form elements, which we’ll learn about in part 3, but one more location pseudo-class that’s worth knowing about is the :target selector.
URLs can contain what’s known as a fragment; it always appears right at the end of the URL, preceded by a # character, and it says to the browser “hey, load this page, and once it’s loaded, please find the element with this ID, and scroll it into view”. For example, this URL will open the MDN documentation for the :target pseudo-class, and jump to the Description section:
Using the :target pseudo-class, we can apply specific styles to the element that matches the current URL fragment, which can be really useful for navigation, or for highlighting a user’s journey through some kind of step-by-step process:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Target Pseudo Class</title>
<style>
h3:target {
background-color: gold;
}
</style>
</head>
<body>
<nav>
<a href="#a">alpha</a>
<a href="#b">bravo</a>
<a href="#c">charlie</a>
<a href="#d">delta</a>
</nav>
<h3 id="a">Alpha</h3>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
<h3 id="b">Bravo</h3>
<p>Sed ut perspiciatis unde omnis iste natus error sit voluptatem.</p>
<h3 id="c">Charlie</h3>
<p>Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis.</p>
<h3 id="d">Delta</h3>
<p>Quis autem vel eum iure reprehenderit qui in ea voluptate velit.</p>
</body>
</html>Attribute Selectors
If you’ve ever used XSLT to transform XML documents, then (1) you have my sympathies, and (2) you’re going to find this next part very familiar.
Let’s learn about CSS attribute selectors.
Attribute selectors give us a way to target elements based on the contents of their HTML attribute values.
Let’s say we’ve got a page about TV shows, containing a bunch of links, and we want to highlight links based on where they go.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Attribute Selectors 1</title>
<style>
/* TODO: highlight links based on where they go */
</style>
</head>
<body>
<h2>TV Time!</h2>
<p>Hey! Let's learn about awesome TV shows.
The OG sci-fi TV show is, of course, <a href="https://startrek.com/"
title="the one with the spaceship">Star Trek</a>,
which premiered in 1964 and is still going strong.</p>
<p>Then there's iconic 80s shows like
<a href="https://en.wikipedia.org/wiki/Knight_Rider_(1982_TV_series)">Knight
Rider</a>,
then we're going to learn about
<a href="https://tvtropes.org/pmwiki/pmwiki.php/Series/StreetHawk"
title="the one with the motorbike">Street
Hawk</a>,
<a href="https://en.wikipedia.org/wiki/Airwolf">Airwolf</a>,
<a href="https://tvtropes.org/pmwiki/pmwiki.php/Series/MagnumPI"
title="the one with the mustache">Magnum
P.I.</a>,
<a href="https://en.wikipedia.org/wiki/The_Bionic_Woman">the
Bionic Woman</a>, and
<a href="https://tvtropes.org/pmwiki/pmwiki.php/Series/Automan"
title="the one with the holographic car">Automan</a>.
Then we'll learn about some of the more obscure shows like
<a href="https://en.wikipedia.org/wiki/Manimal">Manimal</a>,
<a href="https://TVTROPES.ORG/pmwiki/pmwiki.php/Anime/TheMysteriousCitiesOfGold"
title="Mysterious Cities of Gold TV Tropes page">Mysterious
Cities of Gold</a>,
and <a href="https://en.wikipedia.org/wiki/Voltron">Voltron.</a>
</p>
</body>
</html>The requirements are:
- Links with a title attribute should be in italics
- Links to the exact URL
"https://startrek.com/"should be highlighted inskyblue - Links to any page on TV Tropes should be highlighted in
pink - Links that go to Wikipedia should be highlighted in
palegreen
First, create in a rule that’ll match any link with a title attribute:
a[title] {
font-style: italic;
}
Next, a rule that’ll match the href property exactly:
a[href="https://startrek.com/"] {
background-color: skyblue;
}
Match any link whose href attribute starts with "https://tvtropes.org/" - if you know regular expressions, you’ll recognise the caret ^ meaning “match the start”:
a[href^="https://tvtropes.org"] {
background-color: pink;
}
And finally, any link whose href contains the text "wikipedia":
a[href*="wikipedia"] {
background-color: palegreen;
}
Hang on, though… there’s a link to the TV Tropes page for Mysterious Cities of Gold there, and it’s not pink. We might click it by mistake! But - if you take a close look at that link, and you’ll se that the domain name is uppercase. Somebody’s linked to TVTROPES.ORG instead of tvtropes.org — and the href attribute value in HTML is case sensitive. Fortunately, there’s an easy fix - add an i (for insensitive) inside the [ ] for the selector rule:
a[href^="https://tvtropes.org" i] {
background-color: pink;
}
Case sensitivity for attribute selectors is complicated. The type selector - the element name - and the attribute name are always case-insensitive, but the attribute value in HTML is case sensitive, unless it’s one of the 46 special cases defined in the HTML living standard. As we just learned, if you need to make a rule case-insensitive, you can add an i to the selector rule, and if you need to override the default for one of those 46 special cases, you can add an s to the selector rule.
We need three more attribute selectors to collect the whole set.
First, the $= operator, which will match the end of an attribute value. For example, we could use this to add a background-color property to any image on our site which is in .png format:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Attribute Selector: PNG images</title>
<style>
img[src$=".png"] {
background-color: orange;
}
</style>
</head>
<body>
<img src="transparent-images-are-fun.png">
<img src="transparent-images-are-fun.gif">
</body>
</html>Next up, there’s the ~= operator, which will match any word that appears in the attribute value. This one’s a little more esoteric, but say we had a list of employees on our intranet, and each employee’s element had an HTML data-job-roles attribute listing all the things that employee’s qualified to do, and we wanted to highlight the people with First Aid training:
<!DOCTYPE html>
<html lang="en">
<head>
<title>CSS Attributes: Job Roles</title>
<style>
li[data-job-roles~="first-aid"] {
color: red;
font-weight: bold;
}
</style>
</head>
<body>
<ul>
<li data-job-roles="it first-aid">Alice</li>
<li data-job-roles="hr">Bob</li>
<li data-job-roles="finance first-aid admin">Carol</li>
<li data-job-roles="warehouse truck-driver first-aid">Dave</li>
<li data-job-roles="truck-driver">Eliza</li>
</ul>
</body>
</html>Finally, there’s the |= operator. This one’s even more specialised: it’ll match any attribute value, or the first part of an attribute value if the value’s followed by a hyphen. It’s designed specifically to match ISO language codes, as in this example:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Attribute Selectors: ISO Language Codes</title>
<style>
p[lang|="en"] { color: red; }
p[lang|="fr"] { color: blue; }
p[lang|="sr"] { color: purple; }
</style>
</head>
<body>
<p>This paragraph does not specify a language.</p>
<p lang="en">This paragraph is in English but does not specify a region.</p>
<p lang="en-GB">This paragraph is in British English. Hello there.</p>
<p lang="en-AU">This paragraph is in Australian English. G'day mate.</p>
<p lang="fr-FR">Ce paragraphe est en français. Il fait très froid aujourd’hui.</p>
<p lang="fr-CA">Ce paragraphe est en Québecois. Y fait frette en maudit aujourd’hui.</p>
<p lang="sr-RS">Овај пасус је на српском језику.</p>
<p lang="sr-Latn-RS">Ovaj pasus je na srpskom jeziku.</p>
</body>
</html>Structural Selectors
Structural selectors match elements based on the structure of the document. Hence the name.
You want to highlight the first item in a list? Use the :first-child selector.
ul :first-child { color: royalblue; }
You want to highlight the first element in a section, but only if it’s an h2? You can combine type selectors with structural selectors:
section h2:first-child { color: red; }
Say we wanted to put a bottom border on every paragraph except the last one in a section - with structural selectors, that’s easy; we put a bottom border on every paragraph, and then use the :last-child selector to remove it from the last one:
<!DOCTYPE html>
<html lang="en">
<head>
<title>:last-child</title>
<style>
body {
max-width: 720px;
margin: 20px auto;
}
h1 {
text-align: center;
}
section p {
padding-bottom: 8px;
border-bottom: 4px double black;
&:last-child {
border-bottom: none;
}
}
</style>
</head>
<body>
<section>
<h1>Chapter 1</h1>
<p>Professor Algernon Whistlebottom adjusted his steam-powered monocle and
peered at the temporal displacement device that had materialized in his
laboratory, which bore an uncanny resemblance to a 1980s synthesizer with
too many knobs and a suspicious amount of hair metal stickers.</p>
<p>The device began playing "Pour Some Sugar on Me" at precisely 147
decibels while simultaneously opening a portal to the Cretaceous period,
where a rather confused Tyrannosaurus Rex was apparently headbanging to
the rhythm and wearing what appeared to be leather pants made from the
hide of an interdimensional whale.</p>
<p>Meanwhile, in the corner of the laboratory, a sentient Victorian
automaton named Reginald was having an existential crisis about whether
his brass gears constituted a proper rhythm section, all while a colony of
time-traveling moths from the year 3047 had begun choreographing an
elaborate dance routine around his copper-plated head.</p>
<p>The Professor's assistant, a genetically modified octopus named Gerald
who had developed an inexplicable taste for power ballads and afternoon
tea, began frantically scribbling calculations on the wall with his
tentacles, occasionally pausing to air-guitar with remarkable precision
despite lacking the appropriate number of fingers.</p>
<p>As the fabric of space-time continued to unravel to the tune of "Rock of
Ages," Professor Whistlebottom couldn't help but wonder if perhaps he
should have read the instruction manual before plugging in the device,
especially the part about avoiding concerts from the 1980s while operating
experimental technology designed by mildly drunk Victorian inventors.</p>
</section>
<small>Text created by Copilot / Claude Sonnet 4. Prompt: <q>five paragraphs
of weird historical science fiction. Think Guillermo del Toro meets
Douglas Adams at a Def Leppard concert.</q></small>
</body>
</html>Notice that we’ve used a nested CSS rule there, and the actual selector syntax is
&:last-child? The&there is used to combine the enclosing rule with an additional selector - the rule says “hey, apply these rules to all paragraphs – and if one of those paragraphs is a last-child, then also apply these extra rules”
There’s also the :only-child selector, which you can use to find lonely elements with no brothers and sisters:
<!DOCTYPE html>
<html lang="en">
<head>
<title>:only-child</title>
<style>
dd span:only-child {
color: red;
}
</style>
</head>
<body>
<h2>Fictional Families:</h2>
<dl>
<dt>The Simpsons:</dt>
<dd>
<span>Bart</span> <span>Lisa</span> <span>Maggie</span>
</dd>
<dt>The Potters:</dt>
<dd>
<span>Harry</span>
</dd>
<dt>The Skywalkers:</dt>
<dd>
<span>Luke</span> <span>Leia</span>
</dd>
<dt>The Odinssons:</dt>
<dd>
<span>Thor</span> <span>Loki</span> <span>Hela</span>
</dd>
</dl>
</body>
</html>nth-child selectors
One scenario I’ve encountered countless times in my own career is zebra striping - shading alternate rows of a table to improve readability. The CSS nth-child selectors make this kind of thing really straightforward: they’ll target elements based on their index, and they support various formulae you can use to target repeating elements.
The classic zebra-stripe table example uses :nth-child(even):
<!DOCTYPE html>
<html lang="en">
<head>
<title>Zebra Stripes</title>
<style>
table { width: 100%; }
tbody tr:nth-child(3n+7) {
background-color: silver;
}
</style>
</head>
<body>
<table>
<thead>
<tr>
<th>Artist</th>
<th>Title</th>
<th>Year</th>
<th>Chart</th>
</tr>
</thead>
<tbody>
<tr>
<td>The Police</td>
<td>Every Breath You Take</td>
<td>1983</td>
<td>1</td>
</tr>
<tr>
<td>Survivor</td>
<td>Eye of the Tiger</td>
<td>1982</td>
<td>1</td>
</tr>
<tr>
<td>Foreigner</td>
<td>I Want to Know What Love Is</td>
<td>1984</td>
<td>1</td>
</tr>
<tr>
<td>U2</td>
<td>With or Without You</td>
<td>1987</td>
<td>1</td>
</tr>
<tr>
<td>Def Leppard</td>
<td>Pour Some Sugar on Me</td>
<td>1987</td>
<td>2</td>
</tr>
<tr>
<td>Def Leppard</td>
<td>Love Bites</td>
<td>1988</td>
<td>1</td>
</tr>
<tr>
<td>Bon Jovi</td>
<td>Livin’ on a Prayer</td>
<td>1986</td>
<td>4</td>
</tr>
<tr>
<td>Van Halen</td>
<td>Jump</td>
<td>1984</td>
<td>4</td>
</tr>
<tr>
<td>Guns N’ Roses</td>
<td>Sweet Child o’ Mine</td>
<td>1988</td>
<td>24</td>
</tr>
<tr>
<td>Europe</td>
<td>The Final Countdown</td>
<td>1986</td>
<td>3</td>
</tr>
<tr>
<td>Poison</td>
<td>Every Rose Has Its Thorn</td>
<td>1988</td>
<td>13</td>
</tr>
<tr>
<td>AC/DC</td>
<td>You Shook Me All Night Long</td>
<td>1980</td>
<td>12</td>
</tr>
<tr>
<td>Iron Maiden</td>
<td>Run to the Hills</td>
<td>1982</td>
<td>7</td>
</tr>
<tr>
<td>Journey</td>
<td>Don’t Stop Believin’</td>
<td>1981</td>
<td>44</td>
</tr>
<tr>
<td>Scorpions</td>
<td>Rock You Like a Hurricane</td>
<td>1984</td>
<td>58</td>
</tr>
<tr>
<td>Ozzy Osbourne</td>
<td>Crazy Train</td>
<td>1980</td>
<td>56</td>
</tr>
<tr>
<td>Twisted Sister</td>
<td>We’re Not Gonna Take It</td>
<td>1984</td>
<td>66</td>
</tr>
<tr>
<td>Mötley Crüe</td>
<td>Dr. Feelgood</td>
<td>1989</td>
<td>37</td>
</tr>
<tr>
<td>Whitesnake</td>
<td>Here I Go Again</td>
<td>1987</td>
<td>10</td>
</tr>
<tr>
<td>Bryan Adams</td>
<td>Summer of ’69</td>
<td>1984</td>
<td>5</td>
</tr>
</tbody>
</table>
</body>
</html>but you can also stripe :nth-child(3n) to target every third row, :nth-last-child(odd) to target odd-numbered rows starting from the end and working backwards, :nth-child(3n+7) to target the seventh row and every third row thereafter…
of-type selectors
OK, but what if you want to, say alternate the images in a section, so they float left, then right, then left, even if there are other elements between them? Or you want a special rule to apply to an image if it’s the only image in that section, even if the section also includes paragraphs and lists and other elements?
That’s what the of-type selectors are for.
<!DOCTYPE html>
<html lang="en">
<head>
<title>:of-type</title>
<style>
body {
max-width: 480px;
margin: 1em auto;
text-align: justify;
}
img {
width: 100px;
}
section {
overflow: auto;
border-bottom: 2px solid black;
clear: both;
img {
margin-bottom: 1em;
}
}
/* begin examples of of-type selectors */
section {
img:nth-of-type(odd) {
float: right;
margin-left: 1em;
}
img:nth-of-type(even) {
float: left;
margin-right: 1em;
}
img:only-of-type {
display: block;
width: 100%;
margin: 0;
float: none;
}
}
/* end examples of of-type selectors */
</style>
</head>
<body>
<section>
<h1>Cats of Unsplash</h1>
<p>This section includes three cats</p>
<img src="cats/alex-meier-KGiQFgF7dkc-unsplash.jpg"
alt="Cat by Alex Meier on Unsplash">
<p>Cats are fascinating creatures known for their independence, agility, and
mysterious nature. They have been companions to humans for thousands of
years, originally valued for their hunting skills in keeping homes free of
rodents.</p>
<img src="cats/amber-kipp-75715CVEJhI-unsplash.jpg"
alt="Cat by Amber Kipp on Unsplash">
<p>These remarkable felines possess an incredible sense of balance and
flexibility,
allowing them to navigate narrow spaces and land gracefully on their feet.
Their
keen senses, particularly their night vision and acute hearing, make them
excellent
hunters even in low-light conditions.</p>
<img src="cats/loan-7AIDE8PrvA0-unsplash.jpg" alt="Cat by Loan on Unsplash">
<p>In a groundbreaking experiment, Google researchers trained a massive
neural network using 10 million randomly selected YouTube video thumbnails
without any human labeling or guidance. The network, consisting of over 1
billion connections, was left to identify patterns on its own. Remarkably,
one of the first concepts it learned to recognize was cats - developing
what researchers called a "cat neuron" that activated specifically when
feline features were detected. This unsupervised learning breakthrough
demonstrated that artificial neural networks could spontaneously develop
the ability to identify and categorize objects in much the same way
biological brains do, marking a significant milestone in machine learning
and computer vision.</p>
</section>
<section>
<p>There's only one cat in this section.</p>
<img src="cats/manja-vitolic-gKXKBY-C-Dk-unsplash.jpg"
alt="Cat by Manja Vitolic on Unsplash">
<p>Despite their charm, cats can sometimes display behaviors that seem
deliberately troublesome - knocking items off tables, ignoring their
owners when called, or demanding attention at inconvenient times. These
independent creatures follow their own agenda and aren't afraid to show
their displeasure when things don't go their way.</p>
</section>
<section>
<p>This section includes five cats.</p>
<img src="cats/zoe-gayah-jonker-uhnbTZC7N9k-unsplash.jpg"
alt="Cat by Zoe Gayah Jonker on Unsplash">
<p>Meet this elegant feline captured by Zoe Gayah Jonker, showcasing the
natural grace and poise that cats are renowned for. The lighting perfectly
highlights the cat's distinctive features and alert expression.</p>
<img src="cats/bofu-shaw-ofNMMuTD3kE-unsplash.jpg"
alt="Cat by Bofu Shaw on Unsplash">
<p>Bofu Shaw's stunning photograph captures a cat in its natural
environment, demonstrating the perfect balance between wild instincts and
domestic comfort. The composition beautifully frames the cat's curious and
intelligent gaze.</p>
<img src="cats/kevin-knezic-doyAAwH2AyQ-unsplash.jpg"
alt="Cat by Kevin Knezic on Unsplash">
<p>Kevin Knezic presents this magnificent cat with incredible attention to
detail, showcasing the intricate patterns of its fur and the intensity of
its piercing eyes. This image perfectly captures the mysterious allure
that makes cats so captivating.</p>
<img src="cats/paul-hanaoka-o6RbK3y7mK4-unsplash.jpg"
alt="Cat by Paul Hanaoka on Unsplash">
<p>Paul Hanaoka's artistic vision brings out the playful yet dignified
nature of this beautiful cat. The photograph emphasizes the cat's natural
athleticism and the graceful way it carries itself through any
environment.</p>
<img src="cats/timo-volz-mrTydVjg04o-unsplash.jpg"
alt="Cat by Timo Volz on Unsplash">
<p>Timo Volz captures this ginger cat proudly sitting in his favourite
cardboard box. The image highlights the serene confidence
that defines these magnificent creatures.</p>
</section>
<footer>
<h2>Image Credits</h2>
<p>All cat images are from Unsplash. Credit to:
<a href="https://unsplash.com/photos/KGiQFgF7dkc">Alex Meier</a>
•
<a href="https://unsplash.com/photos/75715CVEJhI">Amber Kipp</a>
•
<a href="https://unsplash.com/photos/7AIDE8PrvA0">Loan</a> •
<a href="https://unsplash.com/photos/gKXKBY-C-Dk">Manja Vitolic</a>
•
<a href="https://unsplash.com/photos/uhnbTZC7N9k">Zoe Gayah
Jonker</a> •
<a href="https://unsplash.com/photos/ofNMMuTD3kE">Bofu Shaw</a>
•
<a href="https://unsplash.com/photos/doyAAwH2AyQ">Kevin Knezic</a>
•
<a href="https://unsplash.com/photos/o6RbK3y7mK4">Paul Hanaoka</a>
•
<a href="https://unsplash.com/photos/mrTydVjg04o">Timo Volz</a>
</p>
</footer>
</body>
</html>The final structure selector is :empty, which – you guessed it – matches an element with no content. The only scenario I’ve seen this used in is a content management system - CMS - based on Markdown, which would translate empty lines into empty paragraphs, so you’d end up with a bunch of empty paragraphs on the page; by using an :empty selector , we could set them to display: none so they’d disappear completely:
<!DOCTYPE html>
<html>
<head>
<title>:empty</title>
<style>
p:empty { display: none; }
</style>
</head>
<body>
<p>This paragraph’s got stuff in it, but the ones below don’t...</p>
<p></p>
<p></p>
<p></p>
<p>Here’s another paragraph with some content.</p>
<p></p>
<p></p>
<p>And another paragraph with some content.</p>
</body>
</html>Match Selectors
OK, scenario - we want to put a border around any section which contains exactly one image. If a section contains more than one image? No border. If a section contains no images? No border.
None of the selectors we’ve seen so far can do this - we can find the image, sure, but then we don’t have any way to step back up the DOM tree and apply a style to the parent element.
Let’s meet a new selector: :has()
<!DOCTYPE html>
<html lang="en">
<head>
<title>:has()</title>
<style>
section {
padding: 5px;
border: 5px solid black;
margin-bottom: 1em;
}
section:has(img:only-of-type) {
border: 5px double red;
}
section:has(:only-child) {
border: 5px dashed green;
}
img {
width: 100px;
height: 100px;
}
</style>
</head>
<body>
<section>
<p>This section has four pies.</p>
<img src="pies/cristina-matos-albers-Ltv7a5m8i4c-unsplash.jpg"
title="Artisan pie by Cristina Matos Albers"
alt="Freshly baked artisan pie captured by Cristina Matos Albers on Unsplash">
<img src="pies/fernando-andrade-_OhdR0TPeUE-unsplash.jpg"
title="Gourmet pie by Fernando Andrade"
alt="Perfectly crafted gourmet pie photographed by Fernando Andrade on Unsplash">
<img src="pies/haberdoedas-PU48E_e2lTY-unsplash.jpg"
title="Traditional pie by Haberdoedas"
alt="Traditional style pie beautifully presented by Haberdoedas on Unsplash">
<img src="pies/katerina-X-rkdWzhFIw-unsplash.jpg"
title="Sweet pie by Katerina"
alt="Tempting sweet pie with elegant presentation by Katerina on Unsplash">
</section>
<section>
<p>This section contains just one delicious pie! 🥧</p>
<img src="pies/christian-dala-0285C0q5VTU-unsplash.jpg"
title="Delicious pie by Christian Dala"
alt="Beautiful homemade pie photographed by Christian Dala on Unsplash">
</section>
<section>
<p>This section doesn't have any pies. ☹️</p>
</section>
<section>
<p>This section has two pies.</p>
<img src="pies/luiz-carlos-santi-OJ9EahT0IAA-unsplash.jpg"
title="Rustic pie by Luiz Carlos Santi"
alt="Rustic homestyle pie captured by Luiz Carlos Santi on Unsplash">
<img src="pies/priscilla-du-preez-Y0G5CporB7U-unsplash.jpg"
title="Classic pie by Priscilla Du Preez"
alt="Classic pie with golden crust by Priscilla Du Preez on Unsplash">
</section>
</body>
</html>:has() is one of the CSS match selectors, along with :is(), :where() and :not()
At a glance, :is() looks kinda useless:
:is(p) {
/* style for paragraph */
}
but what makes it so powerful is being able to build rules that use multiple selectors as part of a composite selector. For example, say we want h1 and h2 elements to be purple, with a red bottom border, but only if they are the first child element of a section, or an article, or a main element.
Using normal selectors, we’d have to write a rule like:
section h1:first-child,
section h2:first-child,
article h1:first-child,
article h2:first-child,
main h1:first-child,
main h2:first-child {
color: purple;
}
Using the :is() selector, we can write the same rule much more succinctly:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Is Selector</title>
<style>
:is(main, article, section) :is(h1, h2):first-child {
color: purple;
border-bottom: 2px solid red;
}
</style>
</head>
<body>
<main>
<h1>This <code><h1></code> is the first child of <code><main></code></h1>
<p>A paragraph.</p>
<article>
<h2>This <code><h1></code> is the first child of an <code><article></code></h2>
<p>This is another paragraph</p>
<section>
<h2>This <code><h2></code> is the first child of a <code><section></code>
</h2>
<p>This is another paragraph</p>
<h2>Another <code><h2></code></h2>
<p>👆🏼 <em>that</em> <code><h2></code> shouldn't have any special styling applied.</p>
</section>
<h2>Another <code><h2></code> that shouldn't have any special style.</h2>
<p>And another paragraph.</p>
<section>
<p>This section starts with a paragraph, so the headings here don't
get any special style because none of them matches the <code>:first-child</code>
selector.
</p>
<h2>(you see?)</h2>
</section>
</article>
</main>
</body>
</html>:where() has exactly the same behaviour as :is(), apart from one very important distinction: it doesn’t affect the specificity of the rule.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Where Vs Is</title>
<style>
p.stanza {
color: purple;
}
p:is(.alpha) {
color: green;
text-decoration: underline;
}
p:where(.bravo) {
color: blue;
font-style: italic;
}
</style>
</head>
<body>
<p class="stanza alpha">Antelopes, aardvarks, applejack trees</p>
<p class="stanza bravo">Beavers, bananas, and banjos and bees.</p>
<p class="bravo">Badgers and bicycles, Bovril and bears</p>
<p class="stanza charlie">Caribou, crocodiles, carpets and chairs</p>
</body>
</html>Finally, there’s the :not() selector, sometimes called the negation pseudoclass. This one can be really useful, but it can also do all kinds of weird stuff.
Earlier, we saw an example of using the :last-child selector to remove a border from the last paragraph on a page — but a much nicer way to achieve the same thing is to use a :not() selector so that the border never gets applied in the first place:
body {
max-width: 720px;
margin: 20px auto;
}
h1 {
text-align: center;
}
section p:not(:last-child) {
padding-bottom: 8px;
border-bottom: 4px double black;
}
<h1>Chapter 1</h1>
<p>Professor Algernon Whistlebottom adjusted his steam-powered monocle and
peered at the temporal displacement device that had materialized in his
laboratory, which bore an uncanny resemblance to a 1980s synthesizer with
too many knobs and a suspicious amount of hair metal stickers.</p>
<p>The device began playing "Pour Some Sugar on Me" at precisely 147
decibels while simultaneously opening a portal to the Cretaceous period,
where a rather confused Tyrannosaurus Rex was apparently headbanging to
the rhythm and wearing what appeared to be leather pants made from the
hide of an interdimensional whale.</p>
<p>Meanwhile, in the corner of the laboratory, a sentient Victorian
automaton named Reginald was having an existential crisis about whether
his brass gears constituted a proper rhythm section, all while a colony of
time-traveling moths from the year 3047 had begun choreographing an
elaborate dance routine around his copper-plated head.</p>
<p>The Professor's assistant, a genetically modified octopus named Gerald
who had developed an inexplicable taste for power ballads and afternoon
tea, began frantically scribbling calculations on the wall with his
tentacles, occasionally pausing to air-guitar with remarkable precision
despite lacking the appropriate number of fingers.</p>
<p>As the fabric of space-time continued to unravel to the tune of "Rock of
Ages," Professor Whistlebottom couldn't help but wonder if perhaps he
should have read the instruction manual before plugging in the device,
especially the part about avoiding concerts from the 1980s while operating
experimental technology designed by mildly drunk Victorian inventors.</p>You can use :not() to crank up the specificity of rules by filtering for non-existent elements:
p#horse {
/* 1-0-0 */
}
p#horse:not(#unicorn):not(#pegasus):not(#centaur) {
/* 4-0-0, because there are four IDs in the selector rule */
}
You can also write completely nonsensical rules using :not(). p:not(p) matches every paragraph which isn’t a paragraph, and not(.foo) matches every single element on the page which doesn’t have the "foo" class – which includes <html>, <body> , and will even match elements which are ordinarily invisible like <head>, <style>, and <meta> tags:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="CSS :not() pseudo-class selector example">
<meta name="author" content="Dylan Beattie">
<title>:not(.foo)</title>
<style>
:not(.foo) {
display: block;
border: 2px solid red;
margin: 4px;
padding: 4px;
}
</style>
</head>
<body>
<p>I'm just a paragraph</p>
<p class="foo">I'm foo!</p>
</body>
</html>Selector Strategies
One of the most powerful principles in software engineering is to do the simplest thing that solves the problem you have right now. The road to development hell is paved with good ideas, abstractions, frameworks, and things which looked like they might be useful later.
As far as CSS selectors are concerned: try to use the fewest, simplest selector rules you possibly can. Target HTML elements based on document structure, rather than adding classes and IDs just so you can target something with a CSS rule. If your site’s basic CSS is built around types and structural selectors, you’re going to find it much easier when you have to implement a special case.
If you need to style different kinds of page within the same application, see if you can do it using a single class on the <html> or the <body> element, and building rules which cascade down from there, instead of adding classes all over the page.
We’ll come back to selector strategies and the architecture of CSS in part 3 of the course, when we talk about building complex interactive applications for the web.
CSS Pseudo-Elements
Pseudo-elements give us a way to target a specific part of an element’s content. The classic example is using ::first-line and ``:first-letter to style up a paragraph of text with a decorative capital initial. You can also use the ::selection` pseudo-element to target the part of the document that’s been selected (e.g. by dragging over it with the mouse) - try highlighting part of this excerpt from Dickens’ “A Tale of Two Cities”:
body {
width: 60%;
max-width: 28em;
margin: 8px auto;
text-align: justify;
line-height: 1.4em;
}
h1,
h2 {
text-align: center;
}
p::first-letter {
font-size: 250%;
}
p::first-line {
font-size: 150%;
font-weight: bold;
}
::selection {
background-color: transparent;
text-decoration: underline wavy green;
}
<h1>Chapter 1</h1>
<h2>The Period</h2>
<p>It was the best of times, it was the worst of times, it was the age of
wisdom, it was the age of foohshness, it was the epoch of belief, it was the
epoch of incredulity, it was the season of Light, it was the season of
Darkness, it was the spring of hope, it was the winter of despair, we had
everything before us, we had nothing before us, we were all going direct to
Heaven, we were all going direct the other way — in short, the period was so
far like the present period, that some of its noisiest authorities insisted
on its being received, for good or for evil, in the superlative degree of
comparison only.</p>The only properties that can be styled with
::selectionarecolor,background-color,text-decorationandtext-shadow. You can’t use::selectionfor anything that could affect the document’s layout, otherwise you end up with text that moves out of the way when you try to click on it, which makes for a truly terrible user experience.
::marker can be used to style the bullets and numbers on HTML lists; we’ll talk about that one in the section on lists and counters.
Then there’s ::placeholder, which we can use to style the placeholder text which appears in certain kinds of form elements when the user hasn’t entered any data yet:
input[type="email"]::placeholder {
font-style: italic;
font-weight: bold;
color: silver;
font-family: monospace;
}
<label for="email-input">Email Address:</label>
<input type="email" id="email-input" placeholder="someone@domain.com">And then there are the two pseudo-elements which aren’t really elements at all… but which you can use to do all kinds of awesome things. They’re the ::before and ::after pseudo-elements, and they’re a little strange because, by default, they don’t exist.
But… CSS defines a property called content, which we can use to inject content into the page as part of the the styling process. The content can’t be HTML - markup isn’t support - but it can be text, including Unicode text — which means we can use emoji — or it can load an external image.
body {
width: 80%;
max-width: 40em;
margin: 0 auto;
text-align: justify;
}
p.positive::before {
content: '👍🏼';
}
p.negative::before {
content: '👎🏼';
}
p.tracks::before {
content: '👉🏼';
}
<h1>Classic Albums Revisited</h1>
<h2>Marillion: <em>Brave</em></h2>
<p class="positive">A masterpiece of progressive rock storytelling, “Brave”
showcases Marillion at their creative peak. The album’s conceptual depth
explores themes of isolation and urban alienation with remarkable
sensitivity, while Steve Hogarth’s vocals soar over intricate musical
arrangements that seamlessly blend atmospheric passages with powerful
crescendos.</p>
<p class="negative">“Brave” suffers from occasional self-indulgence
that may alienate casual listeners. At times the concept feels heavy-handed,
and some tracks meander without clear direction. The album’s ambitious scope
sometimes works against it, creating moments where pretension overshadows
the genuine emotional core that makes Marillion’s best work so compelling.
</p>
<p class="tracks">“The Great Escape”, “Living with the Big Lie”, “Hard as
Love”</p>
<h2>Sky: <em>Sky</em></h2>
<p class="positive">Sky’s debut album is a stunning fusion of classical and
rock elements that opened new possibilities for progressive music. The
virtuosic interplay between John Williams’ classical guitar and Francis
Monkman’s synthesizers creates breathtaking sonic landscapes, while tracks
like “Westway” and “Carillon” demonstrate the band’s ability to craft both
delicate pastoral pieces and powerful, driving compositions that showcase
exceptional musicianship.</p>
<p class="negative">The album occasionally feels overly academic, with some
compositions prioritizing technical prowess over emotional connection.
Certain tracks can feel cold and calculated, lacking the warmth that makes
progressive rock truly engaging. The fusion of classical and rock elements,
while innovative, sometimes creates an uneasy tension that doesn’t fully
resolve, leaving listeners caught between two worlds without a clear musical
identity.</p>
<p class="tracks">“Westway”, “Carillon”, “Where Opposites Meet”</p>
<h2>Britney Spears: <em>Oops!... I Did It Again</em></h2>
<p class="positive">A defining moment in early 2000s pop culture, “Oops!... I
Did It Again” showcases Britney’s evolution from teen sensation to confident
pop star. The album’s infectious hooks and polished production create an
irresistible sonic landscape, while tracks like the title song and
“Stronger” demonstrate her growing vocal maturity and stage presence that
would influence a generation of pop artists.</p>
<p class="negative">“Oops!... I Did It Again” relies heavily on formulaic
production that prioritizes commercial appeal over artistic risk-taking.
Several tracks feel manufactured and lack emotional depth, while the album’s
focus on image and marketing occasionally overshadows the music itself. The
record’s safe approach, while successful, hints at missed opportunities for
more adventurous creative exploration.</p>
<p class="tracks">"Oops!... I Did It Again", "Stronger", "Don't Go Knockin' on
My Door"</p>One incredibly useful application of the ::after pseudo-selector involves another CSS function we haven’t seen yet: the attr() function.
Let’s say we want to add a note to every link on the page showing the web address which that link is pointing to. We can use attr() to extract the content from one of an element’s HTML attributes, which we can then combine with ::after:
p a::after {
content: ' [' attr(href) ']';
font-size: 80%;
}
p a::before {
width: 16px;
height: 16px;
}
<p>
Dylan Beattie is a software developer, speaker, and musician known for his
entertaining and educational presentations about software engineering. He's
particularly famous for his programming-themed parody songs and talks that
blend humor with technical expertise. You can find his content on
<a href="https://youtube.com/@dylanbeattie">YouTube</a>,
check out his code on <a href="https://github.com/dylanbeattie">GitHub</a>,
connect with him professionally on <a
href="https://linkedin.com/in/dylanbeattie">LinkedIn</a>,
and follow him on <a
href="https://bsky.app/profile/dylanbeattie.net">Bluesky</a>.
</p>Exercise
TODO: this!