the short answer
The most useful CSS selectors go beyond tags, classes and ids. nth-child stripes table rows, attribute selectors pick out outside links and PDF downloads, :required marks form fields, + and ~ style what comes after a heading, :has styles a card that contains a picture, and :not(:last-child) adds separators between items. The ten recipes below each show the rule and exactly which elements it matches.
Tags, classes and ids get you a long way in CSS, but some jobs need sharper tools: striping a table, flagging a download, marking required fields or adding separators between tags. This page is a set of ten selector recipes, all used on one small café page, each with the rule and exactly which elements it picks.
If selectors are new to you, start with our guide to CSS selectors, which covers the basic types, combining selectors and which rule wins when two disagree. Everything here builds on that.
The page used in every example
<nav>
<a href="/" aria-current="page">Home</a>
<a href="/menu">Menu</a>
<a href="https://maps.example.com/corner-cafe">Find us</a>
</nav>
<main>
<h1>Corner Café</h1>
<h2>Today’s prices</h2>
<p>Prices include VAT.</p>
<p>Ask us about allergens.</p>
<table>
<tbody>
<tr><td>Tea</td> <td>£2.50</td></tr>
<tr><td>Toastie</td> <td>£5.25</td></tr>
<tr><td>Scone</td> <td>£3.00</td></tr>
<tr><td>Soup</td> <td>£4.80</td></tr>
</tbody>
</table>
<p><a href="/files/menu.pdf">Download the menu</a></p>
<form>
<input name="name" required>
<input name="email" type="email" required>
<input name="guests" type="number">
</form>
<div class="card"><img src="tea.jpg" alt="A pot of tea"><p>Tea for two</p></div>
<div class="card"><p>Gift cards</p></div>
<ul class="tags"><li>Vegan</li><li>Gluten free</li><li>Local</li></ul>
</main>The matches listed under each rule were checked on this exact page in a real browser.
1. Striped table rows
tbody tr:nth-child(even) {
background: #f2f2f2;
}Matched: Toastie £5.25, Soup £4.80:nth-child(even) matches the second, fourth and sixth children, and so on. Use odd to start the stripes on the first row instead. Putting tbody in the selector keeps a header row, if you add one, out of the count.
2. The link to the current page
nav a[aria-current="page"] {
font-weight: bold;
text-decoration: underline;
}Matched: Homearia-current="page" tells screen readers which link is the page you are on. Styling the same attribute means the visual marker and the spoken one can never disagree, and there is no extra class to keep in step.
3. Links to other websites
a[href^="https://"] {
text-decoration-style: dashed;
}Matched: Find us^= means the attribute starts with this text. Links within your own site usually start with a slash, so only outside links match. Many sites also add a small arrow after them with a pseudo-element:
a[href^="https://"]::after {
content: " ↗";
}4. Downloads
a[href$=".pdf"] {
padding-left: 1.2em;
background: url("/icons/pdf.svg") no-repeat left center / 1em;
}Matched: Download the menu$= means ends with. It is a neat way to warn visitors that a link opens a file rather than a page. There is also *=, which matches text anywhere in the attribute.
5. Required fields
input:required {
border-left: 4px solid #e5c07b;
}Matched: name, email:required matches any field with the required attribute, so the style updates itself when you change the form. Pair the colour with a written hint, such as “required” in the label, because colour alone is missed by many people.
6. The paragraph straight after a heading
h2 + p {
font-size: 1.15rem;
}Matched: Prices include VAT.+ means the next element, as long as both have the same parent. It is a classic way to make the first paragraph of a section a little larger, like an introduction.
7. Every paragraph after a heading
h2 ~ p {
max-width: 40em;
}Matched: Prices include VAT., Ask us about allergens., Download the menu~ matches every later sibling, not just the next one. Notice the third match: the paragraph holding the download link comes after the heading too, even though a table sits between them. When a rule catches more than you expected, a missing + or an extra ~ is often why.
8. Cards that contain a picture
.card:has(img) {
display: grid;
grid-template-columns: 120px 1fr;
gap: 1rem;
}Matched: Tea for two:has() looks inside an element and matches it if the selector in the brackets finds something there. Before it existed, styling a parent based on its children needed JavaScript. It works in all major current browsers.
9. Separators between items
.tags li:not(:last-child)::after {
content: " ·";
}The selector before the pseudo-element matches:
.tags li:not(:last-child) {
margin-right: 0.25em;
}Matched: Vegan, Gluten free:not(:last-child) means every item except the last, so the dot appears between items but never dangles at the end. The same idea works for borders between list items or commas between tags.
10. Grouping with :is()
main :is(h1, h2) {
font-family: Georgia, serif;
}Matched: Corner Café, Today’s prices:is() takes a list and matches any of them. Without it, you would write main h1, main h2. The saving is small here, but with three or four parts it keeps long selectors readable.
Mixing recipes together
Each recipe above uses one idea, but selectors combine freely. tbody tr:nth-child(even):hover darkens a striped row only while the pointer is over it. nav a:not([aria-current]) styles every menu link except the current one. .card:has(img) p reaches the paragraph inside a picture card.
Read a long selector from right to left, the way the browser does: first find the paragraphs, then keep only those inside a card, then only cards that contain a picture. Reading it that way also tells you how specific it is. If a combined selector starts to need more than three or four parts, a class on the element is usually clearer.
How to test a selector before you style with it
Open your page in the browser, open the console, and run document.querySelectorAll with the selector in quotes. It returns every element the selector matches, so you can see what a rule will catch before you write a single style. Pseudo-elements such as ::after cannot be tested this way, so test the part before them. Our DOM selector examples show more of this in action.
The Elements panel in Chrome, Edge or Firefox helps too. Click an element and the Styles pane lists every rule that applies to it, with crossed-out lines for the ones that lost.
Common mistakes
Counting with nth-child by class
.special:nth-child(2) does not mean the second element with that class. It means an element that is the second child and also has the class. Use :nth-of-type to count only one kind of tag.
Forgetting quotes in attribute values
a[href$=.pdf] is invalid because of the dot, so the browser ignores the whole rule. Quote the value every time and you never have to remember which characters need it.
A space before a pseudo-class
li :last-child with a space means the last child inside each list item. li:last-child means the list item that is last. One space changes the whole meaning.
Expecting + to skip over things
h2 + p matches nothing if anything, even an image, sits between the heading and the paragraph. Use ~ when other elements may come between them.
A short exercise
Using the café page, write a selector that matches the table rows except the first one, so you can give them a top border. It should match Toastie, Scone and Soup. Try it on your own copy of the page in the browser console with document.querySelectorAll.
One possible answer
tbody tr:not(:first-child) {
border-top: 1px solid #ddd;
}Matched: Toastie £5.25, Scone £3.00, Soup £4.80tbody tr + tr matches the same three rows, because every row except the first has a row straight before it.
Related reading
- CSS selectors explained, the guide these recipes build on.
- DOM selector examples, for using the same selectors from JavaScript.
Your next step
Recipes like these turn a plain page into a polished one. Gwecode’s CSS Level 1 teaches the basic selectors with a live page beside your code, and its level check has you style a profile card. CSS Level 2 then covers counting children, combinators and decorations with before and after, and ends with a responsive recipe board. Every lesson is free. Create a free account to keep your progress, and try the lesson below.
practise this · css · lessonCounting childrenstart the lessonquestions
How do I stripe every other table row in CSS?
Use tbody tr:nth-child(even) or odd and give it a background colour. Put tbody in the selector so the header row is not counted.
How do I style links to other websites?
Match links whose address starts with https:// using a[href^="https://"]. Links within your own site usually start with a slash, so they are left alone.
What is the difference between + and ~ in CSS?
h2 + p matches only the paragraph straight after the heading. h2 ~ p matches every paragraph after it that shares the same parent.
Can CSS select a parent element?
Yes, with :has(). .card:has(img) matches cards that contain a picture. It works in all major current browsers, though very old ones ignore it.
Why does :nth-child not count only my class?
nth-child counts every child of the parent, whatever its class. To count only paragraphs, for example, use :nth-of-type instead.
