33

Line 3 is a hidden <div> . I don't want that one to be taken from the odd/even css rule.

enter image description here

What is the best approach to get this to work?

.hidden {display:none;}
.box:not(.hidden):nth-child(odd)  { background: orange; }
.box:not(.hidden):nth-child(even) { background: green;  }
<div class="wrap">
    <div class="box">1</div>
    <div class="box">2</div>
    <div class="box hidden">3</div>
    <div class="box">4</div>
    <div class="box">5</div>
    <div class="box">6</div>
    <div class="box">7</div>
</div>

http://jsfiddle.net/k0wzoweh/

Note: There can be multiple hidden elements.

caramba
  • 21,963
  • 19
  • 86
  • 127
  • 1
    `:nth-of-type` odd and even, Try this - http://jsfiddle.net/6dnaep2w/ – Anonymous Sep 26 '14 at 11:00
  • 2
    @MaryMelody doesn't work, even in your fiddle. Pseudo-selectors don't stack – Joe Sep 26 '14 at 11:02
  • Can you have a `.visible` class and just do it like that? – Albzi Sep 26 '14 at 11:04
  • 1
    Can you just add an additional hidden div either before or after the other? I am not clear on what problem you are trying to solve and what constraints you are under. If you want the visible ones to alternate and the invisible one will not become visible, you should state so. – Reid Sep 26 '14 at 11:09
  • wow I thought this is it @MaryMelody but it is not :( if I try your fiddle and add another `` right after the hidden one it changes the colors which shouldn't happen ... – caramba Sep 26 '14 at 11:09
  • @Reid I've boxes which can on click change the visiblity to `display:none;` and then the odd/even doesn't work for me .. – caramba Sep 26 '14 at 11:13
  • 1
    :nth-of-type only works on **type**, thus it's looking for `div` elements. If the hidden element would be a `p` instead of a `div`, @MaryMelody's solution would work. – Paul Sep 26 '14 at 11:13
  • Updated my answer - I'm even more sure you can't do it now, because not even `.box[class='box']:nth-of-type(even)` works (stacking pseudo and attribute selectors) – Joe Sep 26 '14 at 11:14

9 Answers9

43

:nth-child() pseudo-class looks through the children tree of the parent to match the valid child (odd, even, etc), therefore when you combine it with :not(.hidden) it won't filter the elements properly.

Alternatively, we could fake the effect by CSS gradient as follows:

.hidden {display:none;}

.wrap {
  line-height: 1.2em;
  
  background-color: orange; 
  background-image: linear-gradient(transparent 50%, green 50%);
  background-size: 100% 2.4em;
}
<div class="wrap">
  <div class="box">xx</div>
  <div class="box">xx</div>
  <div class="box hidden">xx</div>
  <div class="box">xx</div>
  <div class="box">xx</div>
  <div class="box">xx</div>
  <div class="box">xx</div>
</div>
Hashem Qolami
  • 97,268
  • 26
  • 150
  • 164
16

Pseudo-selectors don't stack, so your :not doesn't affect the :nth-child (nor would it affect :nth-of-type etc.

If you can resort to jQuery, you can use the :visible pseudo-selector there, although that's not a part of the CSS spec.

If you're generating the HTML and can change that, you can apply odd/even with logic at run-time, eg in PHP:

foreach ($divs AS $i => $div) {
    echo '<div class="box ' . ($i % 2 ? 'even' : 'odd') . '">x</div>';
}

Even trying to do something tricky like

.box[class='box']:nth-of-type(even)

doesn't work, because the psuedo-selector doesn't even stack onto the attribute selector.

I'm not sure there's any way to do this purely with CSS - I can't think of any right now.

Joe
  • 15,669
  • 4
  • 48
  • 83
  • Applying even/odd class at runtime wouldn't work as OP stated that `div` can be clicked and disappear, thus the colors of following elements should change. Javascript is the only way here. – Paul Sep 26 '14 at 11:17
  • 1
    @Paul in that case, he's already using JS for the click-toggle so using it for maintaining colours shouldn't be an issue, at least :-) – Joe Sep 26 '14 at 11:20
  • 1
    That's what I thought, but without more code provided it's hard to guess. I could make millions as a clairvoyant at SO. :D – Paul Sep 26 '14 at 11:22
  • Correct - simple selectors don't stack. This applies to IDs, classes, attributes and pseudo-classes. Every simple selector operates independently. I'm sure I have this covered somewhere, but oh god, the number of duplicates. – BoltClock Sep 26 '14 at 14:25
  • @BoltClock it's just counter-intuitive, I guess. We're all used to ... well, basically jQuery. Spoiling us with lovely, clean `$('.box:not(.hidden):even')` that just works like you expect, how dare they :P – Joe Sep 26 '14 at 14:29
  • Anyway I found one of the answers I was talking about, here it is: http://stackoverflow.com/a/5546296 There's got to be a way to consolidate all these first, last, nth, odd/even etc questions into a single canonical question. I think this link fits the bill, although I'll still have to edit it to widen its scope to cover the rest. What do you think? – BoltClock Sep 26 '14 at 14:38
  • @BoltClock I'm struggling to see anything not covered by your answer. It's not all immediately obvious to scan, but the issue isn't simple so I don't think that can really be helped. Definitely the most complete explanation of it I've seen. It **may** be worth incorporating Hashem's answer from below, as this kind of behaviour is common for trying to zebra-stripe rows (I'd say the most common situation it's used in), but then you're adding in potentially unnecessary information – Joe Sep 26 '14 at 14:51
10

Here's a CSS-only solution:

.box {
  background: orange;
}

.box:nth-child(even) {
  background: green;
}

.box.hidden {
  display: none;
}

.box.hidden ~ .box:nth-child(odd) {
  background: green;
}

.box.hidden ~ .box:nth-child(even) {
  background: orange;
}
<div class="wrap">
  <div class="box">xx</div>
  <div class="box">xx</div>
  <div class="box hidden">xx</div>
  <div class="box">xx</div>
  <div class="box">xx</div>
  <div class="box">xx</div>
  <div class="box">xx</div>
</div>
Tim
  • 139
  • 1
  • 8
  • 4
    Not working if two hidden elements are one right after the other: https://jsfiddle.net/nuxcskx5/ or if there are multiple `hidden` elements: https://jsfiddle.net/nuxcskx5/1/ – caramba Oct 10 '17 at 08:12
  • Original question doesn't ask for multiple hidden elements. – Tim Oct 10 '17 at 13:44
  • That's right, I've tried to keep the question simple. it's only noted in the comments. will update the question. sorry for that. – caramba Oct 10 '17 at 13:45
  • 1
    Well, you can use this solution with multiple hidden elements. You just have to repeat the reverting of the background-color on every appearance of a hidden element. https://codepen.io/anon/pen/VXRBdR Will lead to ridiculously long selectors, but it works. – Felix Wienberg Apr 11 '18 at 13:05
8

Since my rows are being hidden with js, I found that the easiest approach for me was to just add an additional hidden row after each real row that I hide, and remove the hidden rows when I show the real rows again.

Fateh Khalsa
  • 1,326
  • 1
  • 15
  • 19
4

Hide the rows you want to hide calling .hide() for each table row, then call

$("tr:visible:even").css( "background-color", "" ); // clear attribute for all rows

$("tr:visible:even").css( "background-color", "#ddddff" ); // set attribute for even rows

Add your table name to the selector to be more specific. Using :even makes it skip the Header row.

Community
  • 1
  • 1
user1070356
  • 217
  • 3
  • 2
0

As @Fateh Khalsa pointed out, I had a similar problem and since I was manipulating my table with JavaScript (jQuery to be precise), I was able to do the following:

(Note: This assumes use of JavaScript/jQuery which the OP did not state whether or not would be available to them. This answer assumes yes, it would be, and that we may want to toggle visibility of hidden rows at some point.)

  • Inactive records (identified with the CSS class "hideme") are currently visible.
  • Visitor clicks link to hide inactive records from the list.
  • jQuery adds "hidden" CSS class to "hideme" records.
  • jQuery adds additional empty row to the table immediately following the row we just hid, adding CSS classes "hidden" (so it doesn't show) and "skiprowcolor" so we can easily identify these extra rows.

This process is then reversed when the link is clicked again.

  • Inactive records (identified with the CSS class "hideme") are currently hidden.
  • Visitor clicks link to show inactive records from the list.
  • jQuery removes "hidden" CSS class to "hideme" records.
  • jQuery removes additional empty row to the table immediately following the row we just showed, identified by CSS class "skiprowcolor".

Here's the JavaScript (jQuery) to do this:

// Inactive Row Toggle
$('.toginactive').click(function(e) {
    e.preventDefault();
    if ($(this).hasClass('on')) {
        $(this).removeClass('on');                  // Track that we're no longer hiding rows
        $('.wrap tr.hideme').removeClass('hidden'); // Remove hidden class from inactive rows
        $('.wrap tr.skiprowcolor').remove();        // Remove extra rows added to fix coloring
    } else {
        $(this).addClass('on');                     // Track that we're hiding rows
        $('.wrap tr.hideme').addClass('hidden');    // Add hidden class from inactive rows
        $('.wrap tr.hideme').after('<tr class="hidden skiprowcolor"></tr>');
                                                    // Add extra row after each hidden row to fix coloring
    }
});

The HTML link is simple

<a href="#" class="toginactive">Hide/Show Hidden Rows</a>
DevilBoy
  • 43
  • 6
0

scss for @tim answer's above, to keep class name changes to a minimum

$selector: "box";
$hidden-selector: "hidden";

.#{$selector} {
  background: orange;

  :nth-child(even) {
    background: green;
  }

  &.#{$hidden-selector} {
    display: none;
  }

  &.#{$hidden-selector} ~ {
    .#{$selector} {
      &:nth-of-type(odd) {
        background: green;
      }

      &:nth-of-type(even) {
        background: orange;
      }
    }
  }
}
Ashwin Aggarwal
  • 649
  • 8
  • 21
-1

Another way, albeit on the fringe side, is to have an extra <tbody> and either move or copy rows there. Or, an extra div wrapper if using OPs example. Copying easiest of course in regards to restoring etc.

This approach can be useful in some cases.

Below is a simple example where rows are moved when filtered. And yes, it is ranking of stripper names, found it fitting as we are talking stripes ... hah

const Filter = {
  table: null,
  last: {
    tt: null,
    value: ''
  },
  name: function (txt) {
    let tb_d = Filter.table.querySelector('.data'),
        tb_f = Filter.table.querySelector('.filtered'),
        tr = tb_d.querySelectorAll('TR'),
        f = 0
    ;
    tb_f.innerHTML = '';
    if (txt.trim() == '') {
      tb_d.classList.remove('hide');
    } else {
      txt = txt.toLowerCase();
      for (let i = 0; i < tr.length; ++i) {
        let td = tr[i].querySelectorAll('TD')[1];
        if (td.textContent.toLowerCase().includes(txt)) {
          tb_f.appendChild(tr[i].cloneNode(true));
          f = 1;
        }
      }
      if (f)
        tb_d.classList[f ? 'add' : 'remove']('hide');
    }
  },
  key: function (e) {
    const v = e.target.value;
    if (v == Filter.last.value)
      return;
    Filter.last.value = v;
    clearTimeout(Filter.last.tt);
    Filter.last.tt = setTimeout(function () { Filter.name(v); }, 200);
  }
};

Filter.table = document.getElementById('table');
Filter.table.addEventListener('keyup', Filter.key);
table {
  width: 200px;
  border: 3px solid #aaa;
}
tbody tr { background: #e33; }
tbody tr:nth-child(even) { background: #e3e;  }

.hide { display: none; }
<table id="table">
  <thead>
    <tr><th></th><th><input type="text" id="filter" data-keyup="filter" /></th></tr>
    <tr><th>#</th><th>Name</th></tr>
  </thead>
  <tbody class="filtered">
  </tbody>
  <tbody class="data">
    <tr><td>1</td><td>Crystal</td></tr>
    <tr><td>2</td><td>Tiffany</td></tr>
    <tr><td>3</td><td>Amber</td></tr>
    <tr><td>4</td><td>Brandi</td></tr>
    <tr><td>5</td><td>Lola</td></tr>
    <tr><td>6</td><td>Angel</td></tr>
    <tr><td>7</td><td>Ginger</td></tr>
    <tr><td>8</td><td>Candy</td></tr>
  </tbody>
</table>
user3342816
  • 974
  • 10
  • 24
  • @caramba: The answer is not about filtering, the filtering is to show the result. Added more contrast to rows to make it more clear. – user3342816 Apr 07 '20 at 21:23
-2

You can use another type of CSS selector: tbody > tr:nth-of-type(odd) to only target tr nodes, and then, instead of using class names to hide the rows, simply wrap them with some element (which hides them), so the CSS selector would only match odd table rows:

const searchElem = document.querySelector('input');
const tableElem = document.querySelector('table');
const tableBody = document.querySelector('tbody');

function search() {
  const str = searchElem.value.toLowerCase();
  const rows = tableElem.querySelectorAll('tr');
  
  // remove previous wrappers
  // https://stackoverflow.com/a/48573634/104380
  tableBody.querySelectorAll('div').forEach(w => {
    w.replaceWith(...w.childNodes)
  });
  
  // create a wrapper which hides its content:
  const wrapper = document.createElement("div");
  wrapper.setAttribute('hidden', true);
  
  rows.forEach(function(row){
    const text = row.textContent.toLowerCase();
    
    if (str.length && !text.includes(str)) {
      // replace row with wrapper and put the row inside it
      row.replaceWith(wrapper);
      wrapper.appendChild(row);
    }
  });
}

searchElem.addEventListener('keyup', search);
tbody > tr:nth-of-type(odd) { 
  background: pink 
}
<input type="search" placeholder="search">
<table>
  <tbody>
    <tr><td>Apple<td>220
    <tr><td>Watermelon<td>465
    <tr><td>Orange<td>94
    <tr><td>Pear<td>567
    <tr><td>Cherry<td>483
    <tr><td>Strawberry<td>246
    <tr><td>Nectarine<td>558
    <tr><td>Grape<td>535
    <tr><td>Mango<td>450
    <tr><td>Blueberry<td>911
    <tr><td>Pomegranate<td>386
    <tr><td>Carambola<td>351
    <tr><td>Plum<td>607
    <tr><td>Banana<td>292
    <tr><td>Raspberry<td>912
    <tr><td>Mandarin<td>456
    <tr><td>Jackfruit<td>976
    <tr><td>Papaya<td>200
    <tr><td>Kiwi<td>217
    <tr><td>Pineapple<td>710
    <tr><td>Lime<td>983
    <tr><td>Lemon<td>960
    <tr><td>Apricot<td>647
    <tr><td>Grapefruit<td>861
    <tr><td>Melon<td>226
    <tr><td>Coconut<td>868
    <tr><td>Avocado<td>385
    <tr><td>Peach<td>419
  </tbody>
</table>
vsync
  • 118,978
  • 58
  • 307
  • 400
  • That is an interesting solution but it's arguably more work than just adding/removing a class per row. – gman Sep 22 '22 at 20:28
  • Not that much work :) you don't have a choice anyway since CSS is quite limited, so the best way is to use the `nth-of-type` instead of `nth-child` (which matches all children without caring for their type), and then you to bypass the element type problem, you simply manipulate it to be of different node, instead of toggling the a class. it's a reasonable approach without going full-js with this, maintaining the full list list in-memory and only rendering what is needed. – vsync Sep 22 '22 at 20:33
  • Even on a huge list of items, this should still be performant. – vsync Sep 22 '22 at 20:35