Better Semantics with CSS Combinators & Selectors

Share this article

Key Takeaways

  • CSS is a unique language that is not “logical” in the traditional sense, instead focusing on styling elements. It uses combinators to target and style elements and pseudo-elements, providing flexibility and specificity in design.
  • The Adjacent Sibling Combinator, denoted by a + symbol, selects elements that appear directly after another specified element. This can be used to apply specific styles to elements based on their position in the Document Object Model (DOM).
  • The Child Combinator, represented by a > symbol, targets elements that are direct descendants of a specific element. This is useful for applying styles to top-level elements without affecting their nested elements. Other important CSS tools include attribute selectors that allow for deeper control, and structural pseudo-classes that target elements based on their position within the DOM.
I’m going to begin with a provocative claim: I believe CSS is one of the most difficult-to-master computer languages we have. It doesn’t have a complex syntax and you certainly don’t need a doctorate in IT to understand it. However, it’s one of the only popular languages that isn’t “logical”—and I mean that in the most literal sense. Unlike other familiar web development languages such as JavaScript, PHP or even SQL, problems aren’t worked out via common logic. Spoken algorithms like “if X then do Y, otherwise do Z” or “select all of Y then do X with them” don’t translate to a language like CSS. Simply put; it’s a styling language. A language for designers, not developers. Some of the most experienced programmers I’ve worked with struggle to comprehend CSS for this very reason. The Cascade is a metaphorical term for the syntax behind CSS. It calculates elements such as origin, specificity, order and importance by using special glyphs, known as combinators, to target elements (and pseudo-elements) and style them accordingly. No single article could do CSS syntax the justice it deserves—that’s what W3 Specs and Dan are for. However, assuming you already know a thing or two about CSS, let’s examine some lesser-known combinators and not how, but when to use them. At design school we were all taught about classes and IDs, using . and # respectively, to directly target elements. That’s enough control to build a functional website—but it’s not flexible enough to handle a complete design shift. It also creates a lot more work than needed by using presentational values within markup. Let’s take a look at an alternative approach to targeting those difficult-to-get-to elements.

The Adjacent Sibling Combinator

We’ll kick things off with a selector that’s nice to use in subtle situations—employing the adjacent sibling combinator. The adjacent sibling combinator is denoted by connecting two elements with a + symbol:
h1 + p
This will select all p elements that appear directly after an h1 element in the DOM. Typographic theory suggests that we should indent paragraphs in body copy, but only if they succeed another paragraph. A practical use for this selector, then, is to add text-indent values to paragraphs without targeting the first in a tree, like so;
p + p {
    text-indent: 1em;
    }
That beats styling all paragraphs with text-indent and zeroing out the first one with class="first" any day. Three lines, no classes and solid browser support. If you’re nesting your content-level img tags inside your p tags (and you should be) then we can simply pull their left margins back with a negative value of -1em:
p + p img {
    margin-left: -1em;
    }
Simple enough, right? What if we wanted to style the first line of all paragraphs that directly follow a heading, without affecting any other paragraphs? Once again, we can refrain from using a presentational class to do this. A simple selector made up of the adjacent sibling combinator and a pseudo-element will do the trick:
h1 + p::first-line {
    font-variant: small-caps;
    }
Note: while :first-line is a CSS 2.1 approved pseudo-element, the :: notation has been introduced at CSS level 3 in order to establish a discrimination between pseudo-classes and pseudo-elements.

The Child Combinator

A common markup protocol is to wrap your top-level sections in an element named something along the lines of #page or #wrap:
<div id="page">
    <header></header>

    <article>
        <section id="main"></section>
        <aside></aside>
    </article>
    <footer></footer>

</div>
Regardless of whether you’re using HTML 5 or XHTML 1.1 syntax, this basic format should look familiar to you. If you’re running a fixed-width of 960px and aligning your document to the centre with each element horizontally filling the wrapper, your CSS likely resembles:
#page {
    width: 960px;
    margin: 0 auto;
    }

header,
article,
footer { width: 100%; }
Or perhaps you’re being a bit more specific and prefixing with the #page parent to avoid hitting them when/if outside of this selection:
#page header,
#page article,
#page footer { width: 100%; }
There’s a better way. We’ve all seen the universal element selector; *, likely through a browser reset or similar. When we combine this with the child selector, we can target all elements that are direct descendants of #page without hitting their grandchildren or beyond:
#page > * { width: 100%; }
This will future-proof our document if we ever want to add or withdraw elements from the top-level structure. Referring back to our original markup scheme, this will hit the header, article and footer elements without touching #main and aside within article.

String and Substring Attribute Selectors

Attribute selectors are one of the most powerful we have. They too have been around to some degree since CSS 2.1 and are commonly found in the form of input[type="text"]
or a[href="#top"]. However, CSS3 introduces a deeper level of control in the form of strings and substrings. Note: up until this point, everything we’ve discussed has been CSS 2.1 standard, but we’re now stepping into CSS3 territory. We’ll leave it at the presentational layer, so it’s OK to use these right now. We have four primary attribute string selectors available to us, where ‘v’ = value and ‘a’ = attribute:
  • v is one of a list of whitespace-separated values: element[a~="v"]
  • a begins exactly with v: element[a^="v"]
  • a ends exactly with v: element[a$="v"]
  • a contains value: element[a*="v"]
The potential for attribute string selectors is almost endless, but a perfect example is iconography. Perhaps you have an unordered list of links to your social media profiles:
<ul id="social">
    <li><a href="http://facebook.com/designfestival">Like on Facebook</a></li>
    <li><a href="http://twitter.com/designfestival">Follow on Twitter</a></li>

    <li><a href="http://feeds.feedburner.com/designfestival">RSS</a></li>
</ul>
Styling these is as simple as running a substring query through their href attribute to find a keyword. We can then progressively enhance these links, like so:
#social li a::before {
    content: '';
    background: left 50% no-repeat;
    width: 16px;
    height: 16px;
    }

#social li a[href*="facebook"]::before {
    background-image: url(images/icon-facebook.png);
    }

#social li a[href*="twitter"]::before {
    background-image: url(images/icon-twitter.png);
    }

#social li a[href*="feedburner"]::before {
    background-image: url(images/icon-feedburner.png);
    }
Similarly, we can target all links to PDF documents with the suffix substring selector:
a[href$=".pdf"]::before {
    background-image: url(images/icon-pdf.png);
    }
Browsers that don’t support CSS3 attribute substrings won’t display these icons, but that’s OK—they’re not essential to functionality, they’re just a “nice-to-have”.

Structural Pseudo-Classes

Lastly, I want to outline the benefits of structural pseudo-classes, not to be confused with pseudo-elements or link and state pseudo-classes. We can use these to target elements based on their position within the DOM, rather than their contents. A fine example of when to use a structural pseudo-class can be to target the first (or last) element in a tree of elements, or to alternate between odd and even elements:
<ul>

    <li>List Item 1</li>
    <li>List Item 2</li>
    <li>List Item 3</li>
    <li>List Item 4</li>

    <li>List Item 5</li>
    <li>List Item 6</li>
</ul>

ul li { border-top: 1px solid #DDD; }
ul li:last-child { border-bottom: 1px solid #DDD; }
ul li:nth-child(even) { background: #EEE; }
Note: :first-child is the only pseudo-element available in the CSS 2.1 spec. All other pseudo-elements, including :last-child, are CSS3 standards. The key to structural pseudo-elements, however, is when not to use them. They should be strictly reserved for when selectors relate to the position of an element and not its contents. If an element must be styled in a certain way regardless of its position in the DOM, that’s when you should be using a more meaningful, semantic selector, such as a class, ID or string.

Summary

You may already be using some or all of these combinators and selectors today—perhaps in the correct way, perhaps not—but it doesn’t hurt to have a reminder of when you can be using them instead of applying a class or ID to an element. Because that’s something that even the best of us are guilty of.

Frequently Asked Questions (FAQs) about CSS Combinators and Selectors

What are the different types of CSS combinators?

CSS combinators are used to explain the relationship between two CSS selectors. There are four types of CSS combinators: descendant combinator (space), child combinator (greater than symbol), adjacent sibling combinator (plus symbol), and general sibling combinator (tilde symbol). Each of these combinators helps to select elements based on their relationship with other elements in the HTML document.

How does the child combinator work in CSS?

The child combinator in CSS is represented by the “>” symbol. It selects elements that are direct children of a specific element. For example, if you want to select all the direct child paragraphs of a div element, you would write it as “div > p”. This will only select the paragraph elements that are directly nested within the div element, not any that are nested further within other elements.

What is the difference between a child combinator and a descendant combinator?

The main difference between a child combinator and a descendant combinator lies in the depth of their selection. A child combinator (>) only selects direct children of an element, while a descendant combinator (space) selects all descendants of an element, not just the direct children. This means that the descendant combinator will select elements nested at any level within the specified element, while the child combinator will only select elements that are one level deep.

How do I use the adjacent sibling combinator in CSS?

The adjacent sibling combinator in CSS is represented by the “+” symbol. It selects an element that is directly after another specific element, and both elements share the same parent. For example, if you want to select a paragraph that directly follows a div, you would write it as “div + p”. This will only select the paragraph if it directly follows the div and both are children of the same parent element.

What is the general sibling combinator and how does it work?

The general sibling combinator in CSS is represented by the “~” symbol. It selects all elements that are siblings of a specified element. For example, if you want to select all paragraphs that are siblings of a div, you would write it as “div ~ p”. This will select all paragraph elements that are at the same level as the div, regardless of their order in the HTML document.

Can I combine multiple combinators in a single CSS rule?

Yes, you can combine multiple combinators in a single CSS rule. This allows you to create more specific and complex selections. For example, you could use the child combinator and the adjacent sibling combinator together to select an element that is both a direct child of one element and directly following another element.

What is the specificity of CSS combinators?

CSS combinators themselves do not have any specificity. However, they help to determine the specificity of the selectors they are used with. The specificity of a CSS rule is determined by the types and number of selectors used in it. For example, an ID selector has higher specificity than a class selector, and a class selector has higher specificity than a type selector.

How does the order of selectors affect the CSS rule?

The order of selectors in a CSS rule can affect which elements are selected and how the styles are applied. CSS rules are applied in the order they are defined, from top to bottom. If two rules have the same specificity, the later rule will override the earlier one. However, if a rule has higher specificity, it will override any previous rules, regardless of their order.

Can I use CSS combinators with pseudo-classes and pseudo-elements?

Yes, you can use CSS combinators with both pseudo-classes and pseudo-elements. This allows you to create more specific and complex selections. For example, you could use the child combinator with the :first-child pseudo-class to select the first child of an element.

Are there any browser compatibility issues with CSS combinators?

All modern browsers fully support CSS combinators. However, some older versions of Internet Explorer (IE 8 and earlier) do not support the general sibling combinator. It’s always a good idea to check the browser compatibility of any CSS features you use to ensure your website works correctly for all users.

Chris SealeyChris Sealey
View Author

Chris is a UI designer and front-end developer with a passion for all things design, code, caffeine and music. He is currently freelancing and working on a series of side-projects while maintaining a full-time gig at a Sydney-based design studio. Check out his work at 51bits.com or on Dribbble.

Editor's Choicetutorial
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week