The Evolution of Selectors

Dave Arthur - CSS Brigade - May 29, 2014

Photo: Bjørn Christian Tørrissen

Overview of talk

  1. Brief intro to how CSS evolves
  2. Highlight some level 2/3 selectors
  3. Look at some level 3/4 selectors
  4. Talk about selector performance and maintenance

CSS is constantly evolving

Selectors are now at Level 4. What does that mean?

Since CSS3, the spec is broken up into “modules”, each defining a specific part of CSS. [Click image below to see live W3C page]

Image: Screenshot of current CSS module status - from W3C site May 26, 2014

Selector Levels

Level 1

ID, class, type/tag, descendant combinator, :link, :visited, :active, :first-letter, :first-line

Level 2

Level 1 + universal(*), attribute, more combinators, :hover, :focus, :before, :after, :first-child

Level 3

Level 1 + 2 + structural pseudo-classes incl. :last-child and :nth-child(), UI pseudo-classes, negation (:not()) and :target pseudo-classes

Level 4

Level 1 + 2 + 3 + more UI pseudo-classes, :matches(), :has(), time dimensional pseudo-classes (e.g. text to speech), link pseudo-classes, drag-and-drop-related pseudo-classes, grid-related pseudo-classes

Level 2 Selector Highlights

Child Selector (Combinator)

X > Y

Instead of targeting all descendants of a particular container, it will only target the direct children.

E.g.: Styling a nested news list


<section class="news">
    <ul class="news-list">
      <li>
         <article class="article">
           <h4 class="article-title"><a href="#"></a></h4>
           <figure class="article-thumb"><a href="#"><img></a></figure>
           <p class="article-desc"></p>
           <ul>
             <li><a href="#"></a></li>
             <li><a href="#"></a></li>
           </ul>
         </article>
      </li>
      <li>
         <article class="article">
           <h4 class="article-title"><a href="#"></a></h4>
           <figure class="article-thumb"><a href="#"><img></a></figure>
           <p class="article-desc"></p>
           <ul>
             <li><a href="#"></a></li>
             <li><a href="#"></a></li>
           </ul>
         </article>
      </li>
   </ul>
</section>

Check out the CodePen for full HTML

Using a descendant selector - over-rides needed:


.news-list li {      /* Descendant */
  list-style: none;
  padding: 0.5em;
  margin-bottom: 0.5em;
  border-bottom: 1px solid #d5d5d5;
}

.article li {
  list-style: square;
  font-size: 0.9em;
  padding: 0;          /* over-ride */
  border-bottom: none; /* over-ride */
}

Using a child selector - cleaner:


.news-list > li {    /* Child selector - only selects top-level lis */
  list-style: none;
  padding: 0.5em;
  border-bottom: 1px solid #d5d5d5;
}

.news-list li {       /* Descendant - common property in all lis (DRY) */
  margin-bottom: 0.5em;
}

.article li {
  list-style: square;
  font-size: 0.9em;
  /* no over-ride of bottom border or padding needed */
}

Check out the CodePen for full CSS

Adjacent Sibling Selector
(Combinator)

X + Y

Targets elements (Y) which have a particular element (X) direcly preceeding it.

E.g.: Simple inline menu with visual separators


<nav class="menu" role="navigation">
   <ul class="menu-list">
        <li><a href="#">Home</a></li>
        <li><a href="#">About</a></li>
        <li><a href="#">Work</a></li>
        <li><a href="#">Contact</a></li>
     </ul>
</nav>

.menu-list > li:before {
	content: '\2022';
	padding-right: 0.5em;
	margin-left: 0.5em;   
}
.menu-list > li:first-child:before {
	content: none;
}

Cleaner way using “+” combinator


<nav class="menu" role="navigation">
   <ul class="menu-list">
        <li><a href="#">Home</a></li>
        <li><a href="#">About</a></li>
        <li><a href="#">Work</a></li>
        <li><a href="#">Contact</a></li>
     </ul>
</nav>

.menu-list > li + li:before {
	content: '\2022';
	padding-right: 0.5em;
	margin-left: 0.5em;   
}

Check out the CodePen for example

Attribute selectors (Level 2 & 3)

elem[attr="value"] 2
elem[attr^="str"] 3
elem[attr$="str"] 3
elem[attr*="str"] 3

Full list of attribute selectors: http://dev.w3.org/csswg/selectors4/#attribute-selectors

elem[attr="value"]

Since there are many different input element types (especially with HTML5) this selector is great for targeting specific types.


input[type="text"], input[type="email"] {
	border: 1px solid #999;
	padding: 5px 10px;  
}

input[type="submit"] {
	background-color: #0065BD;
	color: #fff;
	border-radius: 10px; 
	padding: 10px; 
}

elem[attr^="str"]

Selects elements which have an attribute value beginning with a particular substring.

E.g. Styling links with different URLs:

Contact us by:

Secure Form Email

<section class="contact">
    <h4>Contact us by:</h4>
    <a href="https://secure.domain.com/contact">Secure Form</a>
    <a href="mailto:contact@hello.com">Email</a>
</section>

.contact > a:before {
    font-family: 'icomoon';
    /* other font declarations */ 
    margin-right: 0.25em;
}

.contact > a[href^="https://secure.domain.com"]:before {
    content: "\e602";
}

.contact > a[href^="mailto"]:before {
    content: "\e601";
}

elem[attr$="str"]

Selects elements which have an attribute value ending with a particular substring.

E.g. Styling links to different file types:

Download file:

PDF Word

<section class="file-download">
    <h4>Download file:</h4>
    <a href="http://domain.com/downloads/file.pdf">PDF</a>
    <a href="http://domain.com/downloads/file.doc">Word</a>
</section>

.file-download > a:before {
    font-family: 'icomoon';
    /* other font declarations */ 
    margin-right: 0.25em;
}

.file-download > a[href$=".pdf"]:before {
    content: "\e603";
}

.file-download > a[href$=".doc"]:before {
    content: "\e604";
}

elem[attr*="str"]

Selects elements which the substring somewhere in the attribute value.

Great for modular code. E.g. Styling icon links with different types of icons:

Follow us:


<section class="social">
    <h4>Follow us:</h4>
    <a class="social-link" href="#"><i class="icon-font-twitter"></i>Twitter</a>
    <a class="social-link" href="#"><i class="icon-sprite-youtube"></i>YouTube</a>
</section>

i[class*="icon-"] {
    display: inline-block;
}
i[class*="icon-font-"]:before {
    font-family: 'icomoon';
    /* other font declarations */
}
.icon-font-twitter:before {
    content: "\e606";
}
i[class*="icon-sprite-"] {
    background: url(lib/images/icon-sprite.png) no-repeat;
    /* other sprite declarations */
}
.icon-sprite-youtube {
    width: 24px;
    height: 28px;
    background-position: 0 0;
}
.social-link:hover > .icon-sprite-youtube {
    background-position: 0 -28px;
}

Level 3 & 4 Selector Highlights

Structural nth-child() / nth-of-type() 3
Logical :not(), :matches() 3/4
Relational :has() 4

:nth-child()

Now in Level 3 we can choose any child of a containing element.

Full demo and code

HTML:


<section class="character-list">
    <article class="char simpsons">
        <a href="#">
            <img src="homer-simpson.jpg"><q>...</q>
        </a>
    </article>
    <article class="char flandereses">
        <a href="#">
            <img src="maude-flanders.jpg"><q>...</q>
        </a>
    </article>
    <article class="char bouviers">
        <a href="#">
            <img src="selma-bouvier.jpg"><q>...</q>
        </a>
    </article>
    ...
</section>

Default positioning of quote bubble:


.char {
    position: relative;
}
.char q {
    position: absolute;
    top: -70%;
    left: -50%;
    /* other styles */
}
.char q:before, .char q:after {
    position: absolute;
    /* other styles */
}
.char q:before {
    right: 50%;
    bottom: -30px;
    /* other styles */
}
.char q:after {
    right: 52%;
    bottom: -50px;
    /* other styles */
}

nth-child() positioning of quote bubble:


/* Left-most column */
.char:nth-child(6n+1) q {
    left: -100%;
}
.char:nth-child(6n+1) q:after {
    right: 45%;
}
/* 2nd column in from left */
.char:nth-child(6n+2) q {
    left: -85%;
}
.char:nth-child(6n+2) q:after {
    right: 40%;
}
/* Right-most column */
.char:nth-child(6n+6) q {
    left: 20%;
}
.char:nth-child(6n+6) q:before {
   right: 50%;
}
.char:nth-child(6n+6) q:after {
   right: 60%;
}

:nth-child()

We get problems if we mix element types.

HTML:


<section class="character-list">
    <h4 class="section-title">Simpsons</h4>
    <article class="char simpsons">
        <a href="#">
            <img src="homer-simpson.jpg"><q>...</q>
        </a>
    </article>
    ...
    <h4 class="section-title">Flandereses</h4>
    <article class="char flandereses">
        <a href="#">
            <img src="ned-flanders.jpg"><q>...</q>
        </a>
    </article>
    ...
</section>

Fixed with :nth-of-type()

Full demo and code

nth-of-type() positioning of quote bubble:


/* Left-most column */
.char:nth-of-type(6n+1) q {
    left: -100%;
}
.char:nth-of-type(6n+1) q:after {
    right: 45%;
}
/* 2nd column in from left */
.char:nth-of-type(6n+2) q {
    left: -85%;
}
.char:nth-of-type(6n+2) q:after {
    right: 40%;
}
/* Right-most column */
.char:nth-of-type(6n+6) q {
    left: 20%;
}
.char:nth-of-type(6n+6) q:before {
   right: 50%;
}
.char:nth-of-type(6n+6) q:after {
   right: 60%;
}

:not(s) & :not(s1[, s2]*)

As of Level 3 we can exclude an element(s) from selections.
When Level 4 is supported :not() will take a selector list.


<section class="character-list">
    <article class="char simpsons">
        <a href="#">
            <img src="homer-simpson.jpg"><q>...</q>
        </a>
    </article>
    <article class="char flandereses">
        <a href="#">
            <img src="maude-flanders.jpg"><q>...</q>
        </a>
    </article>
    <article class="char bouviers">
        <a href="#">
            <img src="selma-bouvier.jpg"><q>...</q>
        </a>
    </article>
</section>

.char:not(.simpsons) img {
    opacity: 0.3;
}
.char:not(.simpsons) q {
    display: none;
}

:matches(s1[, s2]*)

When supported :matches() will allow us to include a selector or group of selectors in the selection.

Previous slide uses the vendor prefixed :any() selector to simulate what :matches() will do. :matches() is currently not supported in browsers I’ve tested. Note: I would not recommend using the :any() selector as it’s on its way out.


/* Using :matches() - STANDARD but no support yet */
.char:matches(.simpsons, .flandereses) img {
    border-color: #0065BD;
}

/* Using vendor prefixed :any() - NON-STANDARD */
.char:-moz-any(.simpsons, .flandereses) img {
    border-color: #0065BD;
}
.char:-webkit-any(.simpsons, .flandereses) img {
    border-color: #0065BD;
}

/* Using classes */
.simpsons img, .flanders img {
	border-color: #0065BD;
}

One useful application of :matches() would be for styling HTML5 headings. Since the document outline has been revised you can have multiple h1s on a page. Example CSS from MDN doing it the looong way:


/* Level 0 */
h1 {
  font-size: 30px;
}
/* Level 1 */
section h1, article h1, aside h1, nav h1 {
  font-size: 25px;
}
/* Level 2 */
section section h1, section article h1, section aside h1, section nav h1,
article section h1, article article h1, article aside h1, article nav h1,
aside section h1, aside article h1, aside aside h1, aside nav h1,
nav section h1, nav article h1, nav aside h1, nav nav h1, {
  font-size: 20px;
}
/* Level 3 */
/* ... don't even think about it*/

When :matches() is supported:


/* Level 0 */
h1 {
  font-size: 30px;
}
/* Level 1 */
:matches(section, article, aside, nav) h1 {
  font-size: 25px;
}
/* Level 2 */
:matches(section, article, aside, nav)
:matches(section, article, aside, nav) h1 {
  font-size: 20px;
}
/* Level 3 */
:matches(section, article, aside, nav)
:matches(section, article, aside, nav)
:matches(section, article, aside, nav) h1 {
  font-size: 15px;
}

:has(rs1[, rs2]*)

Apparently new this year. When supported the :has() relational pseudo will allow us to select for elements which have a particular relationship to the element(s) passed as parameters.

Examples from W3C spec:

Matches only a elements that contain an img child:


a:has(> img)

Matches a dt element immediately followed by another dt element:


dt:has(+ dt)

Matches section elements that don’t contain any heading elements:


section:not(:has(h1, h2, h3, h4, h5, h6))

Need to support older IE versions?

Selectivzr is JS polyfill for Level 3 selectors.

Selector Performance

Some Considerations

  • Browsers read selectors from right to left
  • Ideally want right-most “key” selector to be specific
  • IDs and classes are most efficient
  • Combinators (descendant, child, etc), attributes, pseudo-classes are not as efficient

I know what you are thinking...

Other Considerations

  • IE6 is dead. IE7 and 8 will be dead soon. Browsers are much better than they used to be!
  • You should probably focus on other web performance best practices first (e.g. minimizing, using fonts/SVG or sprites, optimizing image file sizes, CDNs, caching, etc.)
  • There isn’t one solution–different websites require different strategies
  • Focus on maintainability...

Making your CSS more maintainable

  • Don’t tag qualify id or class selectors
  • Don’t “over-qualify” selectors
  • Minimize selector depth
  • Minimize general descendant/child selectors
  • Modularize code
  • Choose a naming/coding convention (SMACSS, BEM, etc.)

Decide what level of CSS efficiency is right for your site.

Don’t tag qualify id or class selectors


/* Qualified */
div#main-content {}
ul.menu-list {}

/* Unqualified */
#main-content {}
.menu-list {}

Issues

  • Selectors tied to particular mark up pattern
  • Increasing selector specificity

Don’t “over-qualify” selectors


/* Over-qualified */
.nav ul li a {}

/* Better */
.nav a {}

Issues

  • Browsers will need to look up document tree anyways. Adding ul and li not necessary
  • Increasing selector specificity

Minimize selector depth


/* Not great */
.content section ul li a {}

/* Better choices - Add class "list" to ul */
.list > li > a {} 
.list a {}
  • The key is we reduced the number of levels the browser has to walk up.
  • Alternatives? Put class on li or individual a elements. Better efficiency but at cost of maintenance?

Minimize general descendant/child selectors

Especially involving universal selector. E.g.:


#main-content section {}
ul li {}
.nav > * {}

Alternatives? Can you use classes? Better mark up?

Modularize code

  • Keeps selector depth low
  • Great for portability with minimal CSS revisions
  • CSS preprocessors make modularizing easy. Create different partial file for each module.

<article class="news-item">
  <h2 class="news-item-title">Title of Article</h2>
  <a href="#"><img class="news-item-thumb" src="" alt=""></a>
  <p class="news-item-excerpt">Excerpt</p>
</article>

.news-item {}        /* module base class */
.news-item-title {} 
.news-item-thumb {}
.news-item-excerpt {}

Thank you for listening! Questions?

Additional Resources

Photo source: https://www.youtube.com/watch?v=2hngBzDDyFE