Skip to main content
Mohammad Shehadeh — home (MSH monogram, letter M filled with the Palestinian flag)

CSS Selectors: The Magic Behind Styling

Published on
5 min read

How does CSS know which elements to style? Selectors. They're like a GPS for your stylesheet, telling CSS where to apply your rules.

Selectors are patterns that match HTML elements. Whether you style every button, highlight specific cards, or add hover effects, selectors are your toolkit for precise styling.

The Big Picture

Selectors are the bridge between your HTML structure and CSS styling. Master them, and you'll write cleaner, more maintainable CSS.

The Essentials: Basic Selectors

Element Selectors

Target elements by their HTML tag name - the simplest way to style.

style.css
1/* Targets all <p> elements */
2p {
3  color: #333;
4}
5
6/* Targets all heading elements (h1, h2, h3) */
7h1, h2, h3 {
8  font-weight: 600;
9}

Class Selectors

Target elements with specific classes - your go-to for reusable styles.

style.css
1/* Style elements with class "button" */
2.button {
3  padding: 0.75rem 1.5rem;
4  background: #3b82f6;
5  color: white;
6}
7
8/* Multiple classes */
9.button.primary {
10  background: #059669;
11}

ID Selectors

Target unique elements - use sparingly, mainly for JavaScript hooks.

style.css
1/* Targets the unique element with id="header" */
2#header {
3  background: #1f2937;
4  color: white;
5  padding: 1rem;
6}
Watch out

IDs should be unique within a page. Use classes for styling multiple elements, and reserve IDs for JavaScript hooks or unique page elements.

Try It Yourself: Basic Selectors

Edit the CSS below to see how different selectors work in real time.

Preview

Attribute Selectors: Targeting by Properties

Style elements based on their HTML attributes. Useful for forms and dynamic content.

style.css
1/* Has a title attribute (any value) */
2[title] {
3  cursor: help;
4}
5
6/* Title is exactly "Hello" */
7[title="Hello"] {
8  color: blue;
9}
10
11/* Title contains "Hello" anywhere */
12[title*="Hello"] {
13  font-weight: bold;
14}
15
16/* Title starts with "Hello" */
17[title^="Hello"] {
18  text-decoration: underline;
19}
20
21/* Title ends with "World" */
22[title$="World"] {
23  border: 1px solid green;
24}

Pseudo-classes: State-Based Styling

Target elements based on their current state or position in the document.

style.css
1/* Unvisited link */
2a:link {
3  color: #2563eb;
4}
5
6/* Already visited link */
7a:visited {
8  color: #7c3aed;
9}
10
11/* Mouse hovers over the link */
12a:hover {
13  color: #1d4ed8;
14}
15
16/* Link being clicked */
17a:active {
18  color: #1e40af;
19}
20
21/* First item in a list */
22li:first-child {
23  font-weight: bold;
24}
25
26/* Last item in a list */
27li:last-child {
28  border-bottom: none;
29}
30
31/* Every even-numbered item */
32li:nth-child(even) {
33  background: #f3f4f6;
34}

Pseudo-elements: Creating Virtual Elements

Add content or style parts of elements that aren't in your HTML.

style.css
1/* Inserts content before the element */
2.quote::before {
3  content: '"';
4}
5
6/* Inserts content after the element */
7.quote::after {
8  content: '"';
9}
10
11/* Styles only the first line of text */
12p::first-line {
13  font-weight: bold;
14}
15
16/* Styles only the first letter (drop cap effect) */
17p::first-letter {
18  font-size: 3rem;
19  float: left;
20}

Combinators: Connecting Selectors

Define relationships between elements to create precise targeting.

style.css
1/* Descendant: any <p> inside <article> (any depth) */
2article p {
3  margin-bottom: 1rem;
4}
5
6/* Child: only direct <p> children of <article> */
7article > p {
8  font-size: 1.1rem;
9}
10
11/* Adjacent sibling: first <p> immediately after <h2> */
12h2 + p {
13  color: #6b7280;
14}
15
16/* General sibling: all <p> elements after <h2> */
17h2 ~ p {
18  text-indent: 1rem;
19}

Modern Magic: Advanced Selectors

The :has() Selector - Style Parents Based on Children

Style a parent element based on what's inside it.

style.css
1/* Card that contains an image gets a grid layout */
2.card:has(img) {
3  display: grid;
4  grid-template-columns: 200px 1fr;
5}
6
7/* Form group turns red when its input is invalid */
8.form-group:has(input:invalid) {
9  border-left: 3px solid #ef4444;
10}
11
12/* Card gets a shadow when any button inside is hovered */
13.card:has(button:hover) {
14  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
15}
Game Changer

:has() is supported in all modern browsers since 2023.

The :is() Selector - Group Multiple Selectors

Clean up repetitive selectors with :is().

style.css
1/* Without :is() — repetitive selector list */
2h1, h2, h3, h4, h5, h6 {
3  font-family: 'Inter', sans-serif;
4}
5
6/* With :is() — same result, cleaner syntax */
7:is(h1, h2, h3, h4, h5, h6) {
8  font-family: 'Inter', sans-serif;
9}
10
11/* Combine with other selectors for even cleaner code */
12article :is(h1, h2, h3) {
13  color: #1f2937;
14  margin-top: 2rem;
15}

The :where() Selector - Zero Specificity

Like :is(), but with zero specificity. Good for base styles.

style.css
1/* :where() has zero specificity — easy to override */
2:where(h1, h2, h3) {
3  margin: 0;
4  padding: 0;
5}
6
7/* This wins because any class has higher specificity than :where() */
8.custom-heading {
9  margin: 2rem 0;
10}
Pro Tip

Use :is() for normal specificity, :where() for easily overridable base styles.

Focus Selectors - Smart Focus Management

Handle focus states for better UX.

style.css
1/* Highlights the form when any input inside is focused */
2.form:focus-within {
3  border-color: #3b82f6;
4  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
5}
6
7/* Shows outline only for keyboard navigation (Tab key) */
8button:focus-visible {
9  outline: 2px solid #3b82f6;
10  outline-offset: 2px;
11}
12
13/* Hides outline for mouse clicks */
14button:focus:not(:focus-visible) {
15  outline: none;
16}

The :not() Selector - Exclude Elements

Style everything except what you don't want.

style.css
1/* All buttons except those with class "primary" */
2button:not(.primary) {
3  background: #6b7280;
4}
5
6/* All list items except the last one get a bottom border */
7li:not(:last-child) {
8  border-bottom: 1px solid #e5e7eb;
9}
10
11/* All inputs except checkboxes and radios */
12input:not([type="checkbox"]):not([type="radio"]) {
13  width: 100%;
14  padding: 0.5rem;
15}

Container Queries - Size-Based Styling

Style elements based on their container size, not the viewport.

style.css
1/* Define a named container to query against */
2.card-container {
3  container-type: inline-size;
4  container-name: card;
5}
6
7/* When container is at least 400px wide */
8@container card (min-width: 400px) {
9  .card {
10    display: grid;
11    grid-template-columns: 200px 1fr;
12  }
13}
14
15/* When container is at least 600px wide */
16@container card (min-width: 600px) {
17  .card {
18    grid-template-columns: 300px 1fr;
19    gap: 2rem;
20  }
21}
Why Container Queries?

Components adapt to their container size rather than the viewport. Ideal for reusable components.

Specificity: The Rules of CSS Priority

CSS specificity determines which rules win when multiple rules target the same element.

Specificity Hierarchy

From highest to lowest: Inline styles > ID selectors > Class selectors > Element selectors. :where() has zero specificity, while :is() and :has() take the specificity of their most specific argument.

style.css
1/* Specificity: 0-0-1 (one element) */
2p {
3  color: black;
4}
5
6/* Specificity: 0-1-1 (one class + one element) */
7p.highlight {
8  color: yellow;
9}
10
11/* Specificity: 1-0-0 (one ID) */
12#main-text {
13  color: red;
14}
15
16/* Specificity: 1-1-1 (one ID + one class + one element) — WINS! */
17#main-text p.highlight {
18  color: blue;
19}

Real-World Examples

style.css
1.nav-menu {
2  background: #1f2937;
3  padding: 1rem 0;
4}
5
6.nav-menu ul {
7  list-style: none;
8  margin: 0;
9  padding: 0;
10  display: flex;
11  justify-content: center;
12}
13
14.nav-menu a {
15  color: white;
16  text-decoration: none;
17  padding: 0.5rem 1rem;
18  border-radius: 0.25rem;
19  transition: background-color 0.2s;
20}
21
22.nav-menu a:hover {
23  background: #374151;
24}
25
26.nav-menu a.active {
27  background: #3b82f6;
28}

Form Styling

style.css
1.form-group {
2  margin-bottom: 1.5rem;
3}
4
5.form-group label {
6  display: block;
7  margin-bottom: 0.5rem;
8  font-weight: 500;
9  color: #374151;
10}
11
12.form-group input {
13  width: 100%;
14  padding: 0.75rem;
15  border: 1px solid #d1d5db;
16  border-radius: 0.375rem;
17  font-size: 1rem;
18}
19
20/* Highlight input on focus */
21.form-group input:focus {
22  outline: none;
23  border-color: #3b82f6;
24  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
25}
26
27/* Red border for invalid inputs */
28.form-group input:invalid {
29  border-color: #ef4444;
30}
31
32/* Green border for valid inputs */
33.form-group input:valid {
34  border-color: #10b981;
35}

Best Practices

  1. Use classes for styling - More flexible and reusable than IDs
  2. Keep selectors simple - Avoid overly complex selectors
  3. Use meaningful names - Describe purpose, not appearance
  4. Avoid deep nesting - Limit to 1-2 levels
  5. Consider specificity - Know how it affects your rules
Pro Tip

Use CSS custom properties (variables) with selectors to create maintainable and consistent styling.

Wrapping Up

CSS selectors are your toolkit for precise styling. Start with the basics and add advanced techniques as you get comfortable.

The best selector is often the simplest one that does the job. Master these patterns, and you'll write cleaner, more maintainable CSS.

Next Steps

Practice with the interactive examples above, then try building your own components using these selector patterns!

Related Articles

GET IN TOUCH

Let's work together

I build fast, accessible, and delightful digital experiences for the web. Whether you have a project in mind or just want to connect, I'd love to hear from you.

Get in touch

or reach out directly at hello@mohammadshehadeh.com