Stephan Olsen
Stephan Olsen

Reputation: 1815

First-child styling only works on first element

I got some code where I'm selecting some elements by first child, and then styling the accordingly. I noticed that it behaves quite strangely, and the css is only being applied to the first element. I tried to select them both with JS, and it seems like, although I'm doing the exact same thing, just on another element, it doesn't return the div.

Code:

console.log(document.querySelector(".index-0:first-child")); // returns div
console.log(document.querySelector(".index-1:first-child")); // returns null
.index-0:first-child {
  color: green;
}

.index-1:first-child {
  color: blue;
  /* doesn't work */
}
<div class="index-0">
  <p class="message"></p>
  <div class="shout">
    <span>Message 1</span>
  </div>
</div>
<div class="index-1">
  <p class="message"></p>
  <div class="shout">
    <span>Message 2</span>
  </div>
</div>

jsfiddle: https://jsfiddle.net/a4k03eny/1/

Upvotes: 0

Views: 152

Answers (1)

T.J. Crowder
T.J. Crowder

Reputation: 1074355

You're misunderstanding how CSS selectors work. (You're not alone; this is a very frequent misunderstanding.) Compound selectors like .index-1:first-child or div.foo are "AND" operations for their individual parts: .index-1:first-child means that it matches an element that's got the class index-1 and is the first child of its parent. Your .index-1 element isn't the first child in its parent, it's the second child.

In this case, you'd use .index-1 to style it, without the :first-child bit.

(If your goal is to style the first child within .index-1, add a > (a child combinator):

.index-1 > :first-child {
    color: green;
}

...but I don't think that's what you're trying to do. Or you could use a space [a descendant combinator], but that would apply to all elements within .index-1 that were the first child of their parent, which I also suspect isn't what you want to do.)


From you comment:

I want to style the first element that has a new class differently.

You can do that by combining .index-1:first-child with :not(.index-1) + .index-1:

.index-1:first-child, :not(.index-1) + .index-1 {
    color: blue;
}

That latter uses an adjacent sibling combinator to say "apply the rules to an .index-1 that isn't immediately after an .index-1):

console.log(document.querySelector(".index-0:first-child, :not(.index-0) + .index-0"));
console.log(document.querySelector(".index-1:first-child, :not(.index-1) + .index-1"));
.index-0:first-child, :not(.index-0) + .index-0 {
    color: green;
}

.index-1:first-child, :not(.index-1) + .index-1 {
    color: blue;
}
<div class="index-0">
  <p class="message"></p>
  <div class="shout">
    <span>Message 1</span>
  </div>
</div>
<div class="index-1">
  <p class="message"></p>
  <div class="shout">
    <span>Message 2</span>
  </div>
</div>

Of course, that requires that you style them specifically (rules for .index-0, .index-1, etc.). You can't do it generically in CSS, but you could in JavaScript. This looks through all elements with the class index, picks out their index-N class name if they have it, and adds highlight to the first in any given run of them:

var lastClass = null;
document.querySelectorAll(".index").forEach(function(e) {
  var m = e.className.match(/(?:^| )(index-\d+)(?: |$)/);
  var thisClass = m ? m[1] : null;
  if (thisClass != lastClass) {
    lastClass = thisClass;
    e.classList.add("highlight");
  }
});

Example:

var lastClass = null;
document.querySelectorAll(".index").forEach(function(e) {
  var m = e.className.match(/(?:^| )(index-\d+)(?: |$)/);
  var thisClass = m ? m[1] : null;
  if (thisClass != lastClass) {
    lastClass = thisClass;
    e.classList.add("highlight");
  }
});
.highlight {
    color: green;
}
<div class="index index-0">
  <p class="message"></p>
  <div class="shout">
    <span>first .index-0</span>
  </div>
</div>
<div class="index index-0">
  <p class="message"></p>
  <div class="shout">
    <span>second .index-0 in a row</span>
  </div>
</div>
<div class="index index-1">
  <p class="message"></p>
  <div class="shout">
    <span>first .index-1</span>
  </div>
</div>

Note: Many browsers now support forEach on the NodeList returned by querySelectorAll, but not quite all do. You can polyfill it if missing:

if (typeof NodeList !== "undefined" &&
    NodeList.prototype &&
    !NodeList.prototype.forEach) {
    Object.defineProperty(NodeList.prototype, "forEach", {
        value: Array.prototype.forEach
    });
}

Upvotes: 2

Related Questions