19

I want to do a star rating control but I can't seem to find a way to select all previous siblings on hover. Does such thing even exist or do I have to use javascript?

span {
  display:inline-block;
  width: 32px;
  height: 32px;
  background-color:#eee;
}

span:hover {
  background-color:red;
}
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
Makyen
  • 31,849
  • 12
  • 86
  • 121
Jackal
  • 3,359
  • 4
  • 33
  • 78
  • 5
    one thing you can do to style the previous siblings is to apply the style you want for all siblings targeting the parent hovered and then reset the style for the next siblings – arieljuod Sep 14 '20 at 00:58
  • 2
    from the duplicate: https://stackoverflow.com/a/36118012/8620333 and many other (read all of them) – Temani Afif Sep 14 '20 at 19:57
  • 1
    While there is no previous sibling selector in css yet, I was playing with a solution specifically for rendering star ratings with just html+css. May be useful to you if you don't mind a handful of css: https://codepen.io/kamalx/pen/QWNxVxJ – lost-and-found Sep 14 '20 at 20:42
  • @lost-and-found Thanks for your time for providing a solution. It looks interesting but I'm not too keen on using logic in CSS, like the checkbox hacks. In your example you can have the same effect if you just put the radio inside a label, hide the radio and keep label inside the star or button it refers to with the same width and height. This way user clicks the star or a button and the label inside with `for="(radio name)"` will check the radio inside it which is pretty sweet to avoid extra code – Jackal Sep 15 '20 at 15:04
  • About the label, remove the for tag from it – Jackal Sep 15 '20 at 15:46
  • 1
    @Jackal indeed, that's a better solution. – lost-and-found Sep 16 '20 at 03:56
  • 1
    @Jackal Answers should not be edited into the question. If you feel your answer, or a version of it, would benefit the canonical question to which this one is closed as a duplicate, then please add it as an answer on that question. If you want it, you can find the source Markdown text for your answer [here](https://stackoverflow.com/revisions/b5361cd8-b6a7-4a2e-8f49-4aebbaca2b22/view-source). – Makyen Sep 17 '20 at 18:58
  • Worked example: https://codepen.io/muhammad_mabrouk/pen/vYpENjy – Muhammad Mabrouk Mar 16 '22 at 18:46

5 Answers5

11

"I can't seem to find a way to select all previous siblings on hover"

Unfortunately CSS can only target, and therefore select, subsequent elements in the DOM. Thankfully this can, of course, be emulated with CSS or enabled with JavaScript.

First, the CSS approach which requires either CSS Grid or CSS Flexbox in order to adjust the order of elements on the page:

*,
 ::before,
 ::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

div {
  /* to place a 1em gap between items, applicable
     to both Grid and Flexbox: */
  gap: 1em;
  width: 80vw;
  margin: 1em auto;
}

div.withFlex {
  /* Using flexbox layout: */
  display: flex;
  /* In the HTML you might have noticed that the '5 star'
     element comes before the '1 star'...'4 star' element,
     this property reverses the order of the flex-items
     (the <span> elements) in the flex-box layout: */
  flex-direction: row-reverse;
  /* spacing the elements apart, this approach places the
     available space (after the element-sizes have been
     calculated) between the elements: */
  justify-content: space-between;
}

div.withFlex span {
  border: 1px solid #000;
  flex: 1 1 auto;
}


/* here we use Grid layout: */

div.withGrid {
  display: grid;
  /* we force the grid-items (the <span> elements) to
     flow into columns rather than rows: */
  grid-auto-flow: column;
  /* here we cause the layout - again - to be reversed,
     flowing from right-to-left: */
  direction: rtl;
}

div.withGrid span {
  border: 1px solid currentcolor;
  text-align: left;
}


/* here we select the <span> that the user hovers over,
   plus any subsequent siblings, and style them differently;
   as the subsequent elements appear - visually - before the
   hovered-<span> this gives the illusion that we're selecting
   previous elements in the DOM: */

span:hover,
span:hover~span {
  color: #f90;
  border-color: currentcolor;
}
<div class="withFlex">
  <span>5 stars</span>
  <span>4 stars</span>
  <span>3 stars</span>
  <span>2 stars</span>
  <span>1 star</span>
</div>

<div class="withGrid">
  <span>5 stars</span>
  <span>4 stars</span>
  <span>3 stars</span>
  <span>2 stars</span>
  <span>1 star</span>
</div>

In addition to the above, assuming that you want the elements to have the ability to remain selected – while still using CSS & HTML – then using some <input> and <label> elements is also possible:

*,
 ::before,
 ::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

div {
  /* to place a 1em gap between items, applicable
     to both Grid and Flexbox: */
  gap: 1em;
  width: 80vw;
  margin: 1em auto;
}

input[type=radio] {
  position: absolute;
  top: -10000px;
  left: -10000px
}

label {
  border: 1px solid currentcolor;
  cursor: pointer;
}

div.withFlex {
  /* Using flexbox layout: */
  display: flex;
  /* In the HTML you might have noticed that the '5 star'
     element comes before the '1 star'...'4 star' element,
     this property reverses the order of the flex-items
     (the <span> elements) in the flex-box layout: */
  flex-direction: row-reverse;
  /* spacing the elements apart, this approach places the
     available space (after the element-sizes have been
     calculated) between the elements: */
  justify-content: space-between;
}

div.withFlex label {
  flex: 1 1 auto;
}


/* here we use Grid layout: */

div.withGrid {
  display: grid;
  /* we force the grid-items (the <span> elements) to
     flow into columns rather than rows: */
  grid-auto-flow: column;
  /* here we cause the layout - again - to be reversed,
     flowing from right-to-left: */
  direction: rtl;
}

div.withGrid label {
  direction: ltr;
}


/* here we select the <span> that the user hovers over,
   plus any subsequent siblings, and style them differently;
   as the subsequent elements appear - visually - before the
   hovered-<span> this gives the illusion that we're selecting
   previous elements in the DOM: */
label:hover,
label:hover~label {
  color: #f90f;
  border-color: currentcolor;
}

/* here we select all <label> elements that follow an <input>
   of type=radio (using an attribute-selector) which is checked: */
input[type=radio]:checked~label {
  color: #f90c;
  border-color: currentcolor;
}
<div class="withFlex">

  <!-- because we're styling the <label> elements based
       on the state (checked/unchecked) of the <input>
       elements we have to place the relevant <input>
       before the affected <label> in the DOM; which is
       why they precede the element that's being styled.
       While the :focus-within pseudo-class exists there
       is (as yet) no comparable ':checked-within', and
       the :has() pseudo-class does not yet (in 2020)
       exist; JavaScript could be used but this demo is
       to show HTML/CSS methods rather than JS: -->
  <input id="withFlexInput5" type="radio" name="rating1" />
  <label for="withFlexInput5">
    5 stars
  </label>

  <input id="withFlexInput4" type="radio" name="rating1" />
  <label for="withFlexInput4">
    4 stars
  </label>

  <input id="withFlexInput3" type="radio" name="rating1" />
  <label for="withFlexInput3">
    3 stars
  </label>

  <input id="withFlexInput2" type="radio" name="rating1" />
  <label for="withFlexInput2">
    2 stars
  </label>

  <input id="withFlexInput1" type="radio" name="rating1" />
  <label for="withFlexInput1">
    1 star
  </label>
</div>

<div class="withGrid">

  <input id="withGridInput5" type="radio" name="rating2" />
  <label for="withGridInput5">
    5 stars
  </label>

  <input id="withGridInput4" type="radio" name="rating2" />
  <label for="withGridInput4">
    4 stars
  </label>

  <input id="withGridInput3" type="radio" name="rating2" />
  <label for="withGridInput3">
    3 stars
  </label>

  <input id="withGridInput2" type="radio" name="rating2" />
  <label for="withGridInput2">
    2 stars
  </label>

  <input id="withGridInput1" type="radio" name="rating2" />
  <label for="withGridInput1">
    1 stars
  </label>
</div>
David Thomas
  • 249,100
  • 51
  • 377
  • 410
6

// obtain all spans from DOM
const spans = document.querySelectorAll('span');
// set a variable at global scope as indicator
let flag = false; 

// add event listener to each span
spans.forEach((sp, j)=>{
    sp.addEventListener('click', ()=>{
    // if clicked, then not dismissing the background colour after mouse leave
    flag = true;
    // reassign all spans back to original grey
    spans.forEach(dsp=>{
        dsp.style.backgroundColor = '#eee';
    });
    // assign bg to red of the spans from 0 to clicked index
    Array.from(new Array(j+1), (x, i) => i).forEach(ind=>{
        spans[ind].style.backgroundColor = 'red';
    });
  });
    // redo if mouse enters
    sp.addEventListener('mouseenter', ()=>{
    flag = false;
  });
    // if any span is hovered
    sp.addEventListener('mouseover', ()=>{
    // reassign all spans back to original grey
    spans.forEach(dsp=>{
        dsp.style.backgroundColor = '#eee';
    });
    // assign bg to red of the spans from 0 to clicked index
    Array.from(new Array(j+1), (x, i) => i).forEach(ind=>{
        spans[ind].style.backgroundColor = 'red';
    });
  });
  // in moseleave, only save the background colour if click happened
  sp.addEventListener('mouseleave', ()=>{
    if(!flag){
      spans.forEach(dsp=>{
        dsp.style.backgroundColor = '#eee';
      });
    }
  });
});
span {
  display:inline-block;
  width: 32px;
  height: 32px;
  background-color:#eee;
}

span:hover {
  background-color:red;
  opacity: 0.8;
  cursor: pointer;
}
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
Weilory
  • 2,621
  • 19
  • 35
5

There are other CSS solutions here which use Flexbox and CSS Grid.

But, if you're happy to go old school, the same effect can be achieved with:

float: right;

Working Example:

div {
float: left;
width: 180px;
}

span {
float: right;
display: inline-block;
width: 32px;
height: 32px;
margin-left: 4px;
background-color: #eee;
cursor: pointer;
}

span:hover,
span:hover ~ span {
background-color: red;
}
<div>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
Rounin
  • 27,134
  • 9
  • 83
  • 108
4

As suggested in the comments, one approach is to style everything, and then undo the styling for subsequent elements. The only real downside is that if you're over the container div but in between spans, then you get everything styled red. One fix would be to put the spans all on one line, so that there's no whitespace between.

#parent {
  line-height: 0;
  display: inline-block;
}
#parent span {
  display:inline-block;
  width: 32px;
  height: 32px;
}
#parent:hover span {
  background-color:red;
}
#parent:hover span:hover~span {
  background-color:#eee;
}
<span id="parent">
<span></span><span></span><span></span><span></span><span></span>
</span>
Teepeemm
  • 4,331
  • 5
  • 35
  • 58
  • This could be great but needs a bit adjustement if you hover outside the squares all squares will be red. Parent tag probably will need a fixed height and width – Jackal Sep 14 '20 at 14:45
  • 1
    @Jackal I was able to fix that (mostly) by changing the span to a div. But there's still a small area below the boxes where you can still hover over the parent span but not a child span and trigger the coloring. – Teepeemm Sep 14 '20 at 15:44
  • @user4642212 Indeed it does. Thanks for that. – Teepeemm Oct 02 '20 at 00:18
3

The javascript above seems nicer but if you're looking for a pure CSS solution, here's a weird trick.

First you need to use flexbox to reverse the order of elements. Then you can use neighbor selectors + in css to color all the "previous" elements (which are really the next elements).

I realize it's a little silly, maybe but it is a CSS only solution and was kind of fun to try.

And if you need it to be 6 stars or 10, it's not easily extended because you need to paste in a bunch of extra CSS rules. If you are using Sass, you could probably build a function that would generate those rules for you.

UPDATE: I saw @David's post and it's much better - using the ~ selector.

.parent { 
  display: flex;
  flex-direction: row-reverse;
  justify-content: flex-end;
}
.parent span {
  display: block;
  border: 1px solid black;
  width: 32px;
  height: 32px;
  background-color: #eee;
}
span + span { 
  margin-right: 10px;
}

span:hover {
  background-color: red;
}

span:hover + span {
  background-color: red;
}
span:hover + span + span {
  background-color: red;
}
span:hover + span + span + span {
  background-color: red;
}
span:hover + span + span + span + span{
  background-color: red;
}
<div class="parent">
  <span></span>
  <span></span>
  <span></span>
  <span></span>
  <span></span>
</div>
mr rogers
  • 3,200
  • 1
  • 19
  • 31