7

I have a dynamic list that has visible and hidden items:

<ul>
    <li class="hidden">
    <li class="hidden">
    <li>
</ul>

I want to apply style to the first element in the list that is not hidden.

My code for the first element:

ul li:first-child{
  font-size:10px;
}

My code to get the non hidden elements

ul li:not(.hidden){
  font-size:10px;
}

How can I merge the two in a cross browser solution? Ideally it would be something like:

ul li:not(.hidden):first-child

(which doesn't work)

Harry
  • 87,580
  • 25
  • 202
  • 214
sigmaxf
  • 7,998
  • 15
  • 65
  • 125
  • "li:not(.hidden):first-child" is valid but it actually means, select the first-child IF it also doesn't have .hidden class (The first child does have the .hidden class so it selects nothing) – Monfa.red Aug 15 '15 at 05:50

2 Answers2

3

CSS only approach

First child does not work that way. You can read more about it in BoltClock's answer here. It selects the first-child of the parent (that also matches any additional conditions) and cannot select the first element among the children which match the provided condition.

Instead what you could do is first apply the required properties on all li:not(.hidden) elements and then override it with default settings for li:not(.hidden) ~ li:not(.hidden). The second selector means that any .hidden element which is a sibling of another (meaning it is not the first) would get the default setting whereas the first would get the modified setting (red color in the snippet).

The general sibling selector should be used as the adjacent sibling selector (+) may not help you completely because as you can see in the below snippet, it would only select all other elements as long as there is no other .hidden in between.

ul > li:not(.hidden) {
  color: red;
}
ul#one > li:not(.hidden) ~ li:not(.hidden) {
  color: black;
}
ul#two > li:not(.hidden) + li:not(.hidden) { /* wont help if any other hidden elements in between */
  color: black;
}
<ul id='one'>
  <li class="hidden">First hidden</li>
  <li class="hidden">Second hidden</li>
  <li>First not hidden</li>
  <li>Second not hidden</li>
  <li class="hidden">Third hidden</li>
  <li>Third not hidden</li>
</ul>

<ul id='two'>
  <li class="hidden">First hidden</li>
  <li class="hidden">Second hidden</li>
  <li>First not hidden</li>
  <li>Second not hidden</li>
  <li class="hidden">Third hidden</li>
  <li>Third not hidden</li>
</ul>

Using JavaScript/jQuery

I know you have asked for pure CSS selectors but as can been seen from both existing answers, it is not possible to achieve this without doing the override for adjacent siblings. If you want a straight-forward way of handling things then you could look at using JS or jQuery. Below are a couple of sample snippet:

jQuery:

$(document).ready(function() {
  $('ul').each(function() {
    $(this).children('li:not(.hidden):first').css('color', 'red');
  });
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<ul id='one'>
  <li class="hidden">First hidden</li>
  <li class="hidden">Second hidden</li>
  <li>First not hidden</li>
  <li>Second not hidden</li>
  <li class="hidden">Third hidden</li>
  <li>Third not hidden</li>
</ul>

<ul id='two'>
  <li>First not hidden</li>
  <li class="hidden">First hidden</li>
  <li class="hidden">Second hidden</li>
  <li>Second not hidden</li>
  <li>Third not hidden</li>
  <li class="hidden">Third hidden</li>
  <li>Fourth not hidden</li>
</ul>

<ul id='two'>
  <li class="hidden">First hidden</li>
  <li>First not hidden</li>
  <li class="hidden">Second hidden</li>
  <li>Second not hidden</li>
  <li>Third not hidden</li>
  <li class="hidden">Third hidden</li>
  <li>Fourth not hidden</li>
</ul>

JavaScript:

$(document).ready(function() {
  var ulEls = document.querySelectorAll('ul');
  for (var i = 0; i < ulEls.length; i++) {
    ulEls[i].querySelector('li:not(.hidden)').setAttribute('style', 'color: red');
  }
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<ul id='one'>
  <li class="hidden">First hidden</li>
  <li class="hidden">Second hidden</li>
  <li>First not hidden</li>
  <li>Second not hidden</li>
  <li class="hidden">Third hidden</li>
  <li>Third not hidden</li>
</ul>

<ul id='two'>
  <li>First not hidden</li>
  <li class="hidden">First hidden</li>
  <li class="hidden">Second hidden</li>
  <li>Second not hidden</li>
  <li>Third not hidden</li>
  <li class="hidden">Third hidden</li>
  <li>Fourth not hidden</li>
</ul>

<ul id='two'>
  <li class="hidden">First hidden</li>
  <li>First not hidden</li>
  <li class="hidden">Second hidden</li>
  <li>Second not hidden</li>
  <li>Third not hidden</li>
  <li class="hidden">Third hidden</li>
  <li>Fourth not hidden</li>
</ul>

Note I have used .each() function or loops in the above snippets because I wanted to illustrate how it covers multiple cases but you may not need them depending on your usage.

Community
  • 1
  • 1
Harry
  • 87,580
  • 25
  • 202
  • 214
  • although this works, it seems soo hackish to override style on elements that have an adjacent sibling – sigmaxf Aug 15 '15 at 06:07
  • If you can find a better method, please use it :) As far as I know there is no other pure CSS way. – Harry Aug 15 '15 at 06:09
  • @raphadko: As you could see from Kit Sunde's answer also there is currently no pure CSS way other than overriding styles. However, from your previous comment I understand that you aren't too interested in using it. Would you like using JS/jQuery as an alternate? – Harry Aug 15 '15 at 09:48
  • Jquery seems cool, I made some changes to the list so that hidden items will move to a separate list – sigmaxf Aug 15 '15 at 14:32
  • Oh that's good @raphadko. A nice question anyway and looking back at my first comment here it seems a bit hard. Hope you didn't feel offended by the tone :) – Harry Aug 15 '15 at 14:33
  • 1
    no worries man, I like to know when something is impossible to do in the optimal way, then we can start looking for different ways to solve the problem – sigmaxf Aug 15 '15 at 14:46
1

:first-child matches with regards to the parent, not the first matched element. You can achieve the same thing with sibling selector, but you have to specify the base because there's no way of matching just once across the whole list:

ul > li:not(.hidden):first-child,
ul > li.hidden + li:not(.hidden) {
  font-size: 10px;
}
ul > li:not(.hidden) ~ li.hidden + li:not(.hidden) {
  font-size: 1em;
}
<ul>
    <li>1
    <li class="hidden">2
    <li>3
    <li>4
</ul>
<ul>
    <li class="hidden">1
    <li class="hidden">2
    <li>3
    <li>4
</ul>

If not for that :not there would've been a useful a CSS4 future feature nth-match where we would be able to do:

li:nth-match(1 of :not(.hidden)) {
  font-size: 10px;
}

But it will likely not allow nesting :not for performance reasons.

Kit Sunde
  • 35,972
  • 25
  • 125
  • 179
  • While this will work for the exact markup shown in question. It would fail if the first element itself is not hidden. – Harry Aug 15 '15 at 05:43
  • Note that this still doesn't work. See the first snippet and how both 1st and 3rd get styled. – Harry Aug 15 '15 at 05:57
  • @Harry That's intentional. – Kit Sunde Aug 15 '15 at 05:58
  • I don't understand. It may be intentional but it doesn't seem to work for all cases right. Sorry if I am nitpicking, I am not trying to say my answer is the correct one but I am trying to understand if I am missing something here. – Harry Aug 15 '15 at 05:59
  • @Harry I interpreted it as wanting the first element after a `.hidden` element to be styled in each instance of the list. – Kit Sunde Aug 15 '15 at 06:02
  • *I want to apply style to the first element in the list that is not hidden.* - Doesn't seem to be the case. Anyway, lets wait for OP to respond. – Harry Aug 15 '15 at 06:03
  • 1
    Harry is right. I want to get the first element that is not hidden, so if 1 is hidden, it will apply the style to the 2nd. If both 1 and 2 are hidden, will apply to 3, and so on – sigmaxf Aug 15 '15 at 06:04
  • Again, please don't mistake me. Have a look at [this](http://jsfiddle.net/hari_shanx/66ycLkv7/). I don't think all cases can be covered without using general sibling selector but then I could be wrong. – Harry Aug 15 '15 at 06:25
  • @Harry I'm not saying you're wrong. I'm aware of the issue, I'm not done editing yet. – Kit Sunde Aug 15 '15 at 06:30
  • @KitSunde: Yep, as I said there is no way currently other than overriding properties in another selector. But based on OP's comment to my answer it seems like they aren't impressed with that idea :( – Harry Aug 15 '15 at 09:21