Back to top

A Walk Through the CSS Combinators

Published: Dec 30, 2023


Introduction


As a web developer who crafts user experiences, Cascading Style Sheets (CSS) is arguably your most powerful tool. With CSS, you control the presentation and style of your web pages, giving them personality and flair to match their content. Selecting the right HTML elements to style is imperative for bringing your vision to life. That’s where CSS combinators and pseudo-classes shine as an essential skill.


Combinators provide various ways to establish a relationship between selectors to target specific elements on the page. For example, descendant combinators select elements nested within other elements, while sibling combinators select elements next to one another. Pseudo-classes work with standard selectors to style elements based on properties aside from their name, tag, or attributes—states like how the user is interacting with them or their position on the page.


By mastering CSS combinators and pseudo-classes, you unlock levels of specificity and efficiency in styling web pages. This guide will explore the most useful combinators and pseudo-classes, explain how they’re used, and show diverse examples so you can harness their versatility. Let’s dive in!


Descendant Combinator (Space)


The descendant combinator might be the easiest to grasp, but also one of the most widely used. It’s simply represented by a single space between selectors. The descendant combinator selects elements that reside within the first specified selector. For example:


div p {
color: rebeccapurple;
}

This will style all <p> elements that live anywhere under a <div> ancestor, regardless of how deeply nested they may be.


We rely on descendant combinators when we want to target elements that occur within a containing element, but whose nesting we can’t predict or don’t want tightly controlled. Use cases include:


  • Styling lists within sections
  • Styling table cells nested in rows within the table body
  • Applying default styles site-wide by using a selector like body p

Just be aware that overuse of the descendant combinator can result in unintended selectors matching across your site. When accuracy matters more than brevity, opt for child or subsequent combinators instead.


Child Combinator (>)


The child combinator selects elements that are direct children of the specified parent element. For example:


ul > li {
margin-left: 20px;
}

This targets only <li> elements that are nested immediately inside a <ul> parent, without any other containers in between.


The child combinator is useful when you want to:


  • Style elements only when they are immediate children
  • Override styles on immediate children
  • Achieve greater CSS specificity

But take care—it’s easy to break child selector styling by accidentally adding container elements or modifying the document structure.


Adjacent Sibling Combinator (+)


While descendant and child combinators style nested elements, the adjacent sibling combinator targets elements that are beside each other horizontally at the same level. For example:


h2 + p {
text-indent: 15px;  
}

This will indent the first <p> element immediately after any <h2> header, but not other paragraphs lower down.


The adjacent sibling combinator is great for styling callouts to relate an element to the one it immediately follows, such as:


  • An author bio paragraph after a byline
  • A map preview beside an address
  • Explanatory content following document headings

Just know that any elements between the selectors will prevent the style from applying.


General Sibling Combinator (~)


While the adjacent sibling combinator targets elements immediately preceded by the former selector, the general sibling combinator is more flexible. It selects elements that share the same parent element, without considering their order. For example:


h3 ~ p { 
font-size: 14px;
}

This will style all <p> elements with 14px font when they follow an <h3> header under a common parent, regardless of other elements in between.


The general sibling combinator is useful for situations like:


  • Styling secondary paragraphs under a heading
  • Creating space between rows of a matrix by targeting each row
  • Applying styles to figures related to their captions

Unlike child and adjacent sibling combinators which break easily if the document structure changes, the general sibling combinator is more resilient since it does not rely on a fixed position of elements.


Attribute Selectors


While combinators let you target elements based on their location and relationships, attribute selectors style elements based on their attributes and attribute values. For example:


a[target="_blank"] {
padding-right: 20px; 
background: url(external.svg) no-repeat right center;
}

This adds visual indicators when links open content in a new tab.


There are multiple types of attribute selectors available:


  • [attr] - Elements with an attribute name
  • [attr=value] - Exact attribute value match
  • [attr~=value] - Attribute value contains whitespace-separated list
  • [attr|=value] - Attribute value matches given value or starts with value followed by ”-”

And more advanced attribute selectors like [attr^=value] (starts with) and [attr$=value] (ends with).


The possibilities are vast for applying styles based on HTML attributes and values, such as:


  • Links, inputs, media, and more based on type
  • Elements with hidden or disabled attributes
  • Form validation styling through required, :valid, and :invalid

Get familiar with attribute selectors to take your styling to the next level!


Pseudo-classes


While CSS combinators let you target elements based on their position in the document and relationship to other elements, pseudo-classes take things a step further. They style elements not just by their type, attributes or ancestry, but also their state—how and when they are rendered on the page.


For example, it’s simple to style a <button> element just by using:


button {
  /* Styles */  
} 

But to differentiate between default and active button states, you’d use a pseudo-class:


button:active {
  opacity: 0.8; 
}

Some common pseudo-classes you’ll use include:


  • :hover - When user hovers over an element
  • :focus - When element receives focus
  • :visited - Links the user has already visited
  • :active - Element while activated (clicked, etc.)

These allow styling based on temporary state changes triggered by the user’s browser interactivity. Other pseudo-classes like :first-child and :nth-of-type() target elements based on their position within ancestral structures.


With the language and creativity pseudo-classes provide, the presentation possibilities are exponentially greater.


Negation Pseudo-class (:not())


Speaking of creativity, the :not() pseudo-class provides an original way to target elements: by excluding elements that match a selector. For example:


p:not(.intro) {
font-size: 14px;  
}

This will style all <p> elements EXCEPT those with class “intro” at a 14px font size.


Why use :not()? Some cases where it comes in handy:


  • Reset a site-wide style for particular elements
  • Style all elements except those marked for custom treatment
  • Target odd/even elements without complex nth- formulas

The negation pseudo-class keeps your stylesheet DRY by letting later rules override unwanted styles instead of having to redo selections entirely.


Structural Pseudo-classes


Structural pseudo-classes target elements based on their position and order within the document structure, rather than just their type, ID, class, or attributes. They provide powerful selectors relating to nested contextual patterns.


Some commonly used structural pseudo-classes include:

  • :root - The top-level element, usually the <html> element
  • :empty - Elements with no children
  • :target - The current URL hash target
  • :first-of-type - The first sibling of its element type
  • :last-of-type - The last sibling of its element type
  • :nth-of-type() - Elements matching a formula based on its sequence amongst siblings of its type
  • :nth-last-of-type() - Elements matching a formula counting from the last element amongst siblings of its type
  • :only-child - Elements that are the sole child of their parent
  • :only-of-type - Unique elements amongst siblings of its type

Structural pseudo-classes shine for styling nested documents like:

  • Numbered lists and table rows
  • Levels of headers and paragraphs
  • Tree diagrams and nested grids

Without them, such visual formatting would require extensive custom classes and markup.


Conclusion


In this guide, we explored the immense power CSS combinators and pseudo-classes provide for targeting elements on a page. Combinators establish relationships between selectors to precisely pinpoint nested elements, adjacent siblings, and more based on their document positions.


Pseudo-classes take selection beyond just element types and names, to format elements differently based on user interactivity, state changes, uniqueness in structure, and other contextual roles they play.


By mastering these CSS features, you can achieve beautiful styling magic without cluttering HTML with excessive classes and divs. The possibilities are endless for crafting responsive, semantic, resilient designs.


I encourage you to grab some CSS cheatsheets listing all the combinators and pseudo-classes available. Bookmark them and experiment wildly in your projects! With practice, you’ll commit these tools to memory and intuitively reach for them to bring your visions into pixel-perfect reality.