惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

C
Check Point Blog
AI
AI
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
U
Unit 42
Vercel News
Vercel News
Stack Overflow Blog
Stack Overflow Blog
P
Proofpoint News Feed
Microsoft Security Blog
Microsoft Security Blog
The GitHub Blog
The GitHub Blog
WordPress大学
WordPress大学
Martin Fowler
Martin Fowler
博客园 - 【当耐特】
B
Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Apple Machine Learning Research
Apple Machine Learning Research
博客园_首页
F
Full Disclosure
Google DeepMind News
Google DeepMind News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
H
Help Net Security
Recorded Future
Recorded Future
N
News and Events Feed by Topic
雷峰网
雷峰网
V
Vulnerabilities – Threatpost
Schneier on Security
Schneier on Security
aimingoo的专栏
aimingoo的专栏
S
Schneier on Security
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
O
OpenAI News
Project Zero
Project Zero
罗磊的独立博客
G
GRAHAM CLULEY
腾讯CDC
P
Privacy International News Feed
V
V2EX
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Hugging Face - Blog
Hugging Face - Blog
爱范儿
爱范儿
H
Heimdal Security Blog
L
LINUX DO - 热门话题
Forbes - Security
Forbes - Security
美团技术团队
MongoDB | Blog
MongoDB | Blog
Security Latest
Security Latest
M
MIT News - Artificial intelligence
T
Tor Project blog
Cisco Talos Blog
Cisco Talos Blog
宝玉的分享
宝玉的分享
T
Threat Research - Cisco Blogs
TaoSecurity Blog
TaoSecurity Blog

CSS Articles by Temani Afif

Get Ready For the Powerful CSS border-shape Property! | CSS-Tricks Let’s Play With Gap Decorations! How to Control Infinite CSS Animations (Part 2 of 2) How to Control Infinite CSS Animations (Part 1 of 2) Two Circles, One Arrow, and Anchor Positioning Making a Responsive Pyramidal Grid With Modern CSS | CSS-Tricks How to Create a CSS-only Elastic Text Effect Making Complex CSS Shapes Using shape() | CSS-Tricks Responsive Hexagon Grid Using Modern CSS | CSS-Tricks Responsive List of Avatars Using Modern CSS (Part 2) | CSS-Tricks Responsive List of Avatars Using Modern CSS (Part 1) | CSS-Tricks Perfectly Pointed Tooltips: To The Corners Perfectly Pointed Tooltips: All Four Sides Perfectly Pointed Tooltips: A Foundation Sequential linear() Animation With N Elements | CSS-Tricks Infinite Marquee Animation using Modern CSS Better CSS Shapes Using shape() — Part 4: Close and Move | CSS-Tricks Drawing CSS Shapes using corner-shape Better CSS Shapes Using shape() — Part 3: Curves | CSS-Tricks Better CSS Shapes Using shape() — Part 2: More on Arcs | CSS-Tricks Better CSS Shapes Using shape() — Part 1: Lines and Arcs | CSS-Tricks Creating Blob Shapes using clip-path: shape() Creating Flower Shapes using clip-path: shape() Custom progress element using the attr() function A CSS-Only Star Rating Component and More! (Part 2) | CSS-Tricks How to Create Wavy Boxes Using CSS Full-Bleed Layout with Modern CSS Fancy Menu Navigation Using Anchor Positioning | CSS-Tricks How to Create a Zig-Zag Box Using CSS How to Create Zig-Zag CSS Loaders Using One Element Custom Progress Element Using Anchor Positioning & Scroll-Driven Animations How to Create Filling CSS Loaders Using One Element How to Create Curved-Edge and Rounded-Edge Shapes Using CSS CSS Tricks That Use Only One Gradient | CSS-Tricks How to create Shapes with Inner Curves using CSS Mask Custom Range Slider Using Anchor Positioning & Scroll-Driven Animations How to Get the Width/Height of Any Element in Only CSS How Keyboard Navigation Works in a CSS Game How To Create Cut-Out Shapes using The clip-path property The Modern Guide For Making CSS Shapes — Smashing Magazine css-shape.com: The Ultimate Collection of CSS-only Shapes ⚡️ Sliding 3D Image Frames In CSS — Smashing Magazine CSS Tricks To Master The clip-path Property Creating Wavy Circles with Fancy Animations in CSS Modern CSS Tooltips And Speech Bubbles (Part 2) — Smashing Magazine Do you need a Tooltip or a Speech Bubble? I have created 100 using CSS 😲 Modern CSS Tooltips And Speech Bubbles (Part 1) — Smashing Magazine Creating Flower Shapes using CSS Mask & Trigonometric Functions CSS Shapes: The Triangle The Complex But Awesome CSS border-image Property — Smashing Magazine CSS Tricks to add 3D Effects to your Text CSS Responsive Multi-Line Ribbon Shapes (Part 2) — Smashing Magazine CSS Responsive Multi-Line Ribbon Shapes (Part 1) — Smashing Magazine I have made 100+ CSS-only Ribbon Shapes | The Perfect Collection 🎀 How to create a CSS-only infinite scroll animation Re-Creating The Pop-Out Hover Effect With Modern CSS (Part 2) — Smashing Magazine Re-Creating The Pop-Out Hover Effect With Modern CSS (Part 1) — Smashing Magazine How to Create CSS Ribbon Shapes with a Single Element CSS Shapes: The Ribbon Revealing Images With CSS Mask Animations — Smashing Magazine css-loaders.com: The Biggest Collection of Loading Animations (more than 500 🤯) How To Define An Array Of Colors With CSS — Smashing Magazine CSS effects on images II Shines, Perspective, And Rotations: Fancy CSS 3D Effects For Images — Smashing Magazine What's your Lucky CSS Pattern? Check It Now! How to Add a CSS Reveal Animation to Your Images Modern Layouts using CSS Grid I created 100+ unique CSS patterns | The best collection 🤩 How to Create a Custom Range Slider Using CSS CSS Shapes: The Heart CSS Shapes: Polygon & Starburst A Text Reveal Animation using CSS CSS Tip: learn CSS the easy way! How to build a CSS-only accordion How to create Breadcrumb Navigation with CSS Different Ways to Get CSS Gradient Shadows | CSS-Tricks A CSS-only responsive Stepper component A Fancy Hover Effect For Your Avatar | CSS-Tricks How to make a zoom effect using CSS Creating a Custom Cursor using CSS CSS Infinite 3D Sliders | CSS-Tricks CSS Infinite Slider Flipping Through Polaroid Images | CSS-Tricks How to create a responsive sidebar menu using CSS CSS Infinite and Circular Rotating Image Slider | CSS-Tricks Making Static Noise From a Weird CSS Gradient Bug | CSS-Tricks CSS Grid and Custom Shapes, Part 3 | CSS-Tricks Fancy Image Decorations: Outlines and Complex Animations | CSS-Tricks Fancy Image Decorations: Masks and Advanced Hover Effects | CSS-Tricks Fancy Image Decorations: Single Element Magic | CSS-Tricks How to create an infinite image slider using CSS How to Create Wavy Shapes & Patterns in CSS | CSS-Tricks How I Made a Pure CSS Puzzle Game | CSS-Tricks How to create a Tooltip/Speech Bubble using CSS CSS Grid and Custom Shapes, Part 2 | CSS-Tricks CSS Grid and Custom Shapes, Part 1 | CSS-Tricks Zooming Images in a Grid Layout | CSS-Tricks How to create a CSS-only loader with one element Exploring CSS Grid’s Implicit Grid and Auto-Placement Powers | CSS-Tricks How to create background pattern using CSS & conic-gradient Single Element Loaders: The Bars | CSS-Tricks
A CSS-Only Star Rating Component and More! (Part 1) | CSS-Tricks
Temani Afif · 2025-02-28 · via CSS Articles by Temani Afif

Creating a star rating component is a classic exercise in web development. It has been done and re-done many times using different techniques. We usually need a small amount of JavaScript to pull it together, but what about a CSS-only implementation? Yes, it is possible!

Here is a demo of a CSS-only star rating component. You can click to update the rating.

Cool, right? In addition to being CSS-only, the HTML code is nothing but a single element:

<input type="range" min="1" max="5">

An input range element is the perfect candidate here since it allows a user to select a numeric value between two boundaries (the min and max). Our goal is to style that native element and transform it into a star rating component without additional markup or any script! We will also create more components at the end, so follow along.

Note: This article will only focus on the CSS part. While I try my best to consider UI, UX, and accessibility aspects, my component is not perfect. It may have some drawbacks (bugs, accessibility issues, etc), so please use it with caution.

The <input> element

You probably know it but styling native elements such as inputs is a bit tricky due to all the default browser styles and also the different internal structures. If, for example, you inspect the code of an input range you will see a different HTML between Chrome (or Safari, or Edge) and Firefox.

DevTools inspecting an input element, showing the shadow root element parts.

Luckily, we have some common parts that I will rely on. I will target two different elements: the main element (the input itself) and the thumb element (the one you slide with your mouse to update the value).

Our CSS will mainly look like this:

input[type="range"] {
  /* styling the main element */
}

input[type="range" i]::-webkit-slider-thumb {
  /* styling the thumb for Chrome, Safari and Edge */
}

input[type="range"]::-moz-range-thumb {
  /* styling the thumb for Firefox */
}

The only drawback is that we need to repeat the styles of the thumb element twice. Don’t try to do the following:

input[type="range" i]::-webkit-slider-thumb,
input[type="range"]::-moz-range-thumb {
  /* styling the thumb */
}

This doesn’t work because the whole selector is invalid. Chrome & Co. don’t understand the ::-moz-* part and Firefox doesn’t understand the ::-webkit-* part. For the sake of simplicity, I will use the following selector for this article:

input[type="range"]::thumb {
  /* styling the thumb */
}

But the demo contains the real selectors with the duplicated styles. Enough introduction, let’s start coding!

Styling the main element (the star shape)

We start by defining the size:

input[type="range"] {
  --s: 100px; /* control the size*/
  
  height: var(--s);
  aspect-ratio: 5;
  
  appearance: none; /* remove the default browser styles */
}

If we consider that each star is placed within a square area, then for a 5-star rating we need a width equal to five times the height, hence the use of aspect-ratio: 5.

That 5 value is also the value defined as the max attribute for the input element.

<input type="range" min="1" max="5">

So, we can rely on the newly enhanced attr() function (Chrome-only at the moment) to read that value instead of manually defining it!

input[type="range"] {
  --s: 100px; /* control the size*/
  
  height: var(--s);
  aspect-ratio: attr(max type(<number>));
  
  appearance: none; /* remove the default browser styles */
}

Now you can control the number of stars by simply adjusting the max attribute. This is great because the max attribute is also used by the browser internally, so updating that value will control our implementation as well as the browser’s behavior.

This enhanced version of attr() is only available in Chrome for now so all my demos will contain a fallback to help with unsupported browsers.

The next step is to use a CSS mask to create the stars. We need the shape to repeat five times (or more depending on the max value) so the mask size should be equal to var(--s) var(--s) or var(--s) 100% or simply var(--s) since by default the height will be equal to 100%.

input[type="range"] {  
  --s: 100px; /* control the size*/
  
  height: var(--s);
  aspect-ratio: attr(max type(<number>));
  
  appearance: none; /* remove the default browser styles */

  mask-image: /* ... */;
  mask-size: var(--s);
}

What about the mask-image property you might ask? I think it’s no surprise that I tell you it will require a few gradients, but it could also be SVG instead. This article is about creating a star-rating component but I would like to keep the star part kind of generic so you can easily replace it with any shape you want. That’s why I say “and more” in the title of this post. We will see later how using the same code structure we can get a variety of different variations.

Here is a demo showing two different implementations for the star. One is using gradients and the other is using an SVG.

In this case, the SVG implementation looks cleaner and the code is also shorter but keep both approaches in your back pocket because a gradient implementation can do a better job in some situations.

Styling the thumb (the selected value)

Let’s now focus on the thumb element. Take the last demo then click the stars and notice the position of the thumb.

The good thing is that the thumb is always within the area of a given star for all the values (from min to max), but the position is different for each star. It would be good if the position is always the same, regardless of the value. Ideally, the thumb should always be at the center of the stars for consistency.

Here is a figure to illustrate the position and how to update it.

The lines are the position of the thumb for each value. On the left, we have the default positions where the thumb goes from the left edge to the right edge of the main element. On the right, if we restrict the position of the thumb to a smaller area by adding some spaces on the sides, we get much better alignment. That space is equal to half the size of one star, or var(--s)/2. We can use padding for this:

input[type="range"] {  
  --s: 100px; /* control the size */
  
  height: var(--s);
  aspect-ratio: attr(max type(<number>));
  padding-inline: calc(var(--s) / 2);
  box-sizing: border-box;
  
  appearance: none; /* remove the default browser styles */

  mask-image: ...;
  mask-size: var(--s);
}

It’s better but not perfect because I am not accounting for the thumb size, which means we don’t have true centering. It’s not an issue because I will make the size of the thumb very small with a width equal to 1px.

input[type="range"]::thumb {
  width: 1px;
  height: var(--s);  

  appearance: none; /* remove the default browser styles */ 
}

The thumb is now a thin line placed at the center of the stars. I am using a red color to highlight the position but in reality, I don’t need any color because it will be transparent.

You may think we are still far from the final result but we are almost done! One property is missing to complete the puzzle: border-image.

The border-image property allows us to draw decorations outside an element thanks to its outset feature. For this reason, I made the thumb small and transparent. The coloration will be done using border-image. I will use a gradient with two solid colors as the source:

linear-gradient(90deg, gold 50%, grey 0);

And we write the following:

border-image: linear-gradient(90deg, gold 50%, grey 0) fill 0 // 0 100px;

The above means that we extend the area of the border-image from each side of the element by 100px and the gradient will fill that area. In other words, each color of the gradient will cover half of that area, which is 100px.

Do you see the logic? We created a kind of overflowing coloration on each side of the thumb — a coloration that will logically follow the thumb so each time you click a star it slides into place!

Now instead of 100px let’s use a very big value:

We are getting close! The coloration is filling all the stars but we don’t want it to be in the middle but rather across the entire selected star. For this, we update the gradient a bit and instead of using 50%, we use 50% + var(--s)/2. We add an offset equal to half the width of a star which means the first color will take more space and our star rating component is perfect!

We can still optimize the code a little where instead of defining a height for the thumb, we keep it 0 and we consider the vertical outset of border-image to spread the coloration.

input[type="range"]::thumb{
  width: 1px;
  border-image: 
    linear-gradient(90deg, gold calc(50% + var(--s) / 2), grey 0)
    fill 0 // var(--s) 500px;
  appearance: none;
}

We can also write the gradient differently using a conic gradient instead:

input[type="range"]::thumb{
  width: 1px;
  border-image: 
    conic-gradient(at calc(50% + var(--s) / 2), grey 50%, gold 0)
    fill 0 // var(--s) 500px;
  appearance: none;
}

I know that the syntax of border-image is not easy to grasp and I went a bit fast with the explanation. But I have a very detailed article over at Smashing Magazine where I dissect that property with a lot of examples that I invite you to read for a deeper dive into how the property works.

The full code of our component is this:

<input type="range" min="1" max="5">
input[type="range"] {  
  --s: 100px; /* control the size*/
  
  height: var(--s);
  aspect-ratio: attr(max type(<number>));
  padding-inline: calc(var(--s) / 2); 
  box-sizing: border-box; 
  appearance: none; 
  mask-image: /* ... */; /* either an SVG or gradients */
  mask-size: var(--s);
}

input[type="range"]::thumb {
  width: 1px;
  border-image: 
    conic-gradient(at calc(50% + var(--s) / 2), grey 50%, gold 0)
    fill 0//var(--s) 500px;
  appearance: none;
}

That’s all! A few lines of CSS code and we have a nice rating star component!

Half-Star Rating

What about having a granularity of half a star as a rating? It’s something common and we can do it with the previous code by making a few adjustments.

First, we update the input element to increment in half steps instead of full steps:

<input type="range" min=".5" step=".5" max="5">

By default, the step is equal to 1 but we can update it to .5 (or any value) then we update the min value to .5 as well. On the CSS side, we change the padding from var(--s)/2 to var(--s)/4, and we do the same for the offset inside the gradient.

input[type="range"] {  
  --s: 100px; /* control the size*/
  
  height: var(--s);
  aspect-ratio: attr(max type(<number>));
  padding-inline: calc(var(--s) / 4); 
  box-sizing: border-box; 
  appearance: none; 
  mask-image: ...; /* either SVG or gradients */
  mask-size: var(--s);
}

input[type="range"]::thumb{
  width: 1px;
  border-image: 
    conic-gradient(at calc(50% + var(--s) / 4),grey 50%, gold 0)
    fill 0 // var(--s) 500px;
  appearance: none;
}

The difference between the two implementations is a factor of one-half which is also the step value. That means we can use attr() and create a generic code that works for both cases.

input[type="range"] {  
  --s: 100px; /* control the size*/
  
  --_s: calc(attr(step type(<number>),1) * var(--s) / 2);
  height: var(--s);
  aspect-ratio: attr(max type(<number>));
  padding-inline: var(--_s);
  box-sizing: border-box; 
  appearance: none; 
  mask-image: ...; /* either an SVG or gradients */
  mask-size: var(--s);
}

input[type="range"]::thumb{
  width: 1px;
  border-image: 
    conic-gradient(at calc(50% + var(--_s)),gold 50%,grey 0)
    fill 0//var(--s) 500px;
  appearance: none;
}

Here is a demo where modifying the step is all that you need to do to control the granularity. Don’t forget that you can also control the number of stars using the max attribute.

Using the keyboard to adjust the rating

As you may know, we can adjust the value of an input range slider using a keyboard, so we can control the rating using the keyboard as well. That’s a good thing but there is a caveat. Due to the use of the mask property, we no longer have the default outline that indicates keyboard focus which is an accessibility concern for those who rely on keyboard input.

For a better user experience and to make the component more accessible, it’s good to display an outline on focus. The easiest solution is to add an extra wrapper:

<span>
  <input type="range" min="1" max="5">
</span>

That will have an outline when the input inside has focus:

span:has(:focus-visible) {
  outline: 2px solid;
}

Try to use your keyboard in the below example to adjust both ratings:

Another idea is to consider a more complex mask configuration that keeps a small area around the element visible to show the outline:

mask: 
  /* ... */ 0/var(--s),
  conic-gradient(from 90deg at 2px 2px,#0000 25%,#000 0) 
   0 0/calc(100% - 2px) calc(100% - 2px);

I prefer using this last method because it maintains the single-element implementation but maybe your HTML structure allows you to add focus on an upper element and you can keep the mask configuration simple. It totally depends!

More examples!

As I said earlier, what we are making is more than a star rating component. You can easily update the mask value to use any shape you want.

Here is an example where I am using an SVG of a heart instead of a star.

Why not butterflies?

This time I am using a PNG image as a mask. If you are not comfortable using SVG or gradients you can use a transparent image instead. As long as you have an SVG, a PNG, or gradients, there is no limit on what you can do with this as far as shapes go.

We can go even further into the customization and create a volume control component like below:

I am not repeating a specific shape in that last example, but am using a complex mask configuration to create a signal shape.

Conclusion

We started with a star rating component and ended with a bunch of cool examples. The title could have been “How to style an input range element” because this is what we did. We upgraded a native component without any script or extra markup, and with only a few lines of CSS.

What about you? Can you think about another fancy component using the same code structure? Share your example in the comment section!

Article series

  1. A CSS-Only Star Rating Component and More! (Part 1)
  2. A CSS-Only Star Rating Component and More! (Part 2)