Building a star rating component is a classic exercise in web development and has been repeatedly implemented using a variety of techniques. Usually a small amount of JavaScript code is required, but what about implementations using CSS? Yes, yes!
This is a demonstration of the star rating component implemented using CSS only. You can click Update Rating.
It's cool, right? Besides using CSS only, HTML code is just a single element:
<input type="range">
The range input element is ideal because it allows the user to select a numeric value between two boundaries (minimum and maximum). Our goal is to design this native element, convert it into a star rating component, without additional tagging or any scripts! We will also create more components at the end, so stay tuned.
Note: This article only focuses on the CSS part. While I tried my best to think about UI, UX, and accessibility aspects, my components weren't perfect. It may have some drawbacks (errors, accessibility issues, etc.), so use with caution.
<input type="range">
Element You may know that designing native elements, like input elements, is a bit tricky due to all the default browser styles and different internal structures. For example, if you check the code for a range input element, you will see different HTML between Chrome (or Safari, Edge) and Firefox.
Luckily, we have some parts in common that I will rely on. I'll locate two different elements: main element (input itself) and slider element (that element you use the mouse to swipe to update the value).
Our CSS is mainly as follows:
input[type="range"] { /* 主元素样式 */ } input[type="range"][i]::-webkit-slider-thumb { /* Chrome、Safari和Edge的滑块样式 */ } input[type="range"]::-moz-range-thumb { /* Firefox的滑块样式 */ }
The only downside is that we need to repeat the style of the slider element twice. Do not try to do the following:
input[type="range"][i]::-webkit-slider-thumb, input[type="range"]::-moz-range-thumb { /* 滑块样式 */ }
This does not work because the entire selector is invalid. Chrome etc. do not understand the ::-moz-*
part, while Firefox does not understand the ::-webkit-*
part. For simplicity, in this article I will use the following selector:
input[type="range"]::thumb { /* 滑块样式 */ }
But the demo contains real selectors with repeating styles. The introduction is enough, let's start coding!
We first define the size:
input[type="range"] { --s: 100px; /* 控制大小 */ height: var(--s); aspect-ratio: 5; appearance: none; /* 删除默认浏览器样式 */ }
If we think each star is in a square area, for a 5-star rating, we need width equal to five times the height, so use aspect-ratio: 5
.
The 5 values are also defined as the value of the max
attribute of the input element.
<input type="range" max="5">
So we can rely on the newly enhanced attr()
function (currently Chrome only) to read the value instead of manually defining it!
input[type="range"] { --s: 100px; /* 控制大小 */ height: var(--s); aspect-ratio: attr(max type(<number>)); appearance: none; /* 删除默认浏览器样式 */ }</number>
Now you can control the number of stars by simply adjusting the max
attribute. This is great because the max
attribute is also used internally by the browser, so updating the value will control our implementation and the behavior of the browser.
This enhanced version of attr()
is currently only available in Chrome, so all my demos include a fallback scheme to help unsupported browsers.
The next step is to use a CSS mask to create stars. We need the shape to be repeated five times (or more times, depending on the max
value), so the mask size should be equal to var(--s) var(--s)
or var(--s) 100%
or simplified to var(--s)
because the height will be equal to 100% by default.
<input type="range">
You may ask what is the attribute? I think it's not surprising that I'll tell you it needs some gradient, but it can be an SVG as well. This article is about creating a star rating component, but I want the star part to remain versatile so you can easily replace it with any shape you want. That's why I said "more" in the title of this post. We will see later how to get various different variants using the same code structure. mask-image
In this case, the SVG implementation looks cleaner and the code is shorter, but remember both methods, because gradient implementations can sometimes do better.
Slider style (selected value)
The good news is that for all values (from minimum to maximum), the slider is always within the region of a given star, but each star has a different position. It would be even better if the position is always the same regardless of the value. Ideally, for consistency, the slider should always be in the center of the star.
This is a picture that illustrates the location and how to update it.
These lines are the slider positions for each value. On the left we have the default position where the slider moves from the left edge of the main element to the right edge. On the right, if we limit the slider position to the smaller area by adding some space on both sides, we will get better alignment. This space is equal to half the size of a star, or
. We can use padding for this: var(--s)/2
input[type="range"] { /* 主元素样式 */ } input[type="range"][i]::-webkit-slider-thumb { /* Chrome、Safari和Edge的滑块样式 */ } input[type="range"]::-moz-range-thumb { /* Firefox的滑块样式 */ }
input[type="range"][i]::-webkit-slider-thumb, input[type="range"]::-moz-range-thumb { /* 滑块样式 */ }
You may think we are still far from the end result, but we are almost done! A property is missing to complete this puzzle:
. border-image
The
attribute allows us to draw decorations outside the element using its border-image
function. To do this, I make the slider smaller and transparent. The coloring will be done using outset
. I'm going to use a gradient with two solid colors as the source: border-image
input[type="range"]::thumb { /* 滑块样式 */ }
<input type="range">
The above means that we expand the area of border-image
by 100px from each side of the element, and the gradient will fill that area. In other words, each color of the gradient will cover half of the area, i.e. 100px.
Do you understand the logic? We created an overflowing shading on each side of the slider – a shading that will logically follow the slider so that every time a star is clicked it slides into place!
Now let's use a very large value instead of 100px:
We are almost done! The coloring fills all the stars, but we don't want it to be in the middle, but on the entire selected star. To do this, we update the gradient a little, instead of using 50%, we use 50% var(--s)/2
. We add an offset equal to half the width of the star, which means the first color will take up more space and our star rating component is perfect!
We can still optimize the code slightly instead of defining the height for the slider, we keep it at 0 and consider the vertical border-image
of outset
to extend the shading.
input[type="range"] { /* 主元素样式 */ } input[type="range"][i]::-webkit-slider-thumb { /* Chrome、Safari和Edge的滑块样式 */ } input[type="range"]::-moz-range-thumb { /* Firefox的滑块样式 */ }
We can also use conical gradients to write gradients differently:
input[type="range"][i]::-webkit-slider-thumb, input[type="range"]::-moz-range-thumb { /* 滑块样式 */ }
I know the syntax of border-image
is not easy to understand, and I am a little quick to explain. But I have a very detailed article published on Smashing Magazine where I dissect this property with many examples and I invite you to read it for a deeper understanding of how it works.
The complete code of our component is as follows:
input[type="range"]::thumb { /* 滑块样式 */ }
input[type="range"] { --s: 100px; /* 控制大小 */ height: var(--s); aspect-ratio: 5; appearance: none; /* 删除默认浏览器样式 */ }
That's it! With a few lines of CSS code, we get a nice star rating component!
How to set the score granularity to half-star? This is very common, and we can complete the previous code by making some tweaks.
First, we update the input element to increment in half step instead of full step:
<input type="range" max="5">
By default, the step size is equal to 1, but we can update it to .5 (or any value) and then also update the minimum value to .5. On the CSS side, we change the padding from var(--s)/2
to var(--s)/4
and do the same for offsets in the gradient.
input[type="range"] { --s: 100px; /* 控制大小 */ height: var(--s); aspect-ratio: attr(max type(<number>)); appearance: none; /* 删除默认浏览器样式 */ }</number>
The difference between the two implementations is a one-half factor, which is also the step size value. This means we can use attr()
and create common code that works for both cases.
input[type="range"] { --s: 100px; /* 控制大小 */ height: var(--s); aspect-ratio: attr(max type(<number>)); appearance: none; /* 删除默认浏览器样式 */ mask-image: /* ... */; mask-size: var(--s); }</number>
This is a demo where modifying the step size is everything you need to do to control the granularity. Don't forget that you can also use the max
attribute to control the number of stars.
As you know, we can use the keyboard to adjust the value entered by the range slider, so we can also use the keyboard to control the score. This is a good thing, but there is a warning. Due to the use of the mask
property, we no longer have a default outline indicating the focus of the keyboard, which is an accessibility issue for users who rely on keyboard input.
For a better user experience and make the components more accessible, it is best to display the outline in focus. The easiest solution is to add an extra wrapper:
<input type="range">
This will have an outline when the input inside has focus:
input[type="range"] { /* 主元素样式 */ } input[type="range"][i]::-webkit-slider-thumb { /* Chrome、Safari和Edge的滑块样式 */ } input[type="range"]::-moz-range-thumb { /* Firefox的滑块样式 */ }
Try to use the keyboard to adjust these two ratings in the following example:
Another idea is to consider a more complex mask configuration that keeps small areas around the element visible to show the outline:
input[type="range"][i]::-webkit-slider-thumb, input[type="range"]::-moz-range-thumb { /* 滑块样式 */ }
I prefer to use the last method because it keeps the single element implementation, but maybe your HTML structure allows you to add focus on the parent element, and you can keep the mask configuration simple. It's all up to you!
As I said before, we made more than just a star rating component. You can easily update the mask value to use any shape you want.
Here is an example, I am using the SVG of the heart instead of the star.
Why isn't it a butterfly?
This time I use PNG images as mask. If you are not used to using SVG or gradients, you can use transparent images. As long as you have SVG, PNG, or gradients, you can do anything in shape with this method.
We can further customize and create a volume control component like the following:
In the last example, instead of repeating the specific shape, I used a complex mask configuration to create the signal shape.
We started with a star rating component and ended up with a lot of cool examples. The title could have been “How to design range input elements” because that’s what we do. We upgraded a native component without any scripts or extra tags and used only a few lines of CSS code.
Where are you? Can you think of another beautiful component that uses the same code structure? Share your examples in the comment section!
The above is the detailed content of A CSS-Only Star Rating Component and More! (Part 1). For more information, please follow other related articles on the PHP Chinese website!