Stars on a page are easy to get wrong because two very different widgets look almost the same. Some only report a score. Others collect one.
The report-only kind runs on a percentage stored in a custom property, so 3.7 out of 5 becomes 74%. The collecting kind is a set of hidden radio inputs, with :checked and :has() lighting the stars. HTML and CSS alone can handle both.
The :has() part got easier in June 2026, when it reached Baseline Widely available (web.dev Baseline digest). Stars can now sit in natural order, and the old row-reverse workaround can retire.
Display rating or input rating: which one are you building?
Some stars only report a number, say 4.3 out of 5, that came from somewhere else. Others collect a fresh number from the visitor. Both turn a score into filled and empty stars, but the markup underneath has very little in common.
The read-only version needs no form control at all. A wrapper with an accessible name and a fill that matches the value is enough.
Collecting a score means a group of radio inputs. A widget walkthrough on iamkate.com points out that assistive software reads the group as radio buttons and standard keyboard interaction comes with it, no extra code.
Building that version from span elements and JavaScript click handlers is where most widgets go wrong, according to a 2026 theplusaddons guide. A span has no keyboard behavior and no form value, so both have to be rebuilt by hand. Nobody enjoys that part.
Five stars is the default, but the maximum is a value like any other. A 10-star scale works the same way as long as the markup, not the stylesheet, decides the star count.
An input rating counts among the interactive elements on a page, so it picks up focus and touch requirements that a display rating never has.
A product page showing an average needs the first version. A review form needs the second. Most pages end up with both, built separately.
Star shape methods compared: Unicode, SVG, mask, and icon fonts
See the Pen
CSS Unicode Star Rating by Joseph Fusco (@fusco)
on CodePen.
Unicode gives the smallest build and inline SVG gives an exact brand shape. A mask is what handles partial fills in any color, and icon fonts rarely beat any of the three.
| Method | Color and scaling | Fractional fill | Fails when |
|---|---|---|---|
| Unicode (U+2605, U+2606) | Follows color and font-size | Needs gradient or overlay | Each font draws its own star |
| Inline SVG | Exact shape, fill follows currentColor | Overlay or stacked copies | Fill is hard-coded in the file |
| clip-path or mask-image | Any background shows through | Gradient behind the mask | Older Chrome needs -webkit-mask |
| Icon font | Follows color and font-size | Same limits as Unicode | Extra font request for one glyph |
The two Unicode characters used for ratings are U+2605 (black star, filled) and U+2606 (white star, outline). Both live in the Miscellaneous Symbols block (U+2600 to U+26FF), and U+2605 has been in Unicode since version 1.1 in 1993 (codepoints.net). Insert them with ::before and content: “\2605” and they pick up the text color automatically.
Neither one is a half star. A partial fill needs a gradient or overlay on top, which the next section covers.
Drawing the star as inline SVG gives an exact shape at any size, and fill: currentColor lets it inherit the text color. Where it goes wrong is a fill attribute baked into the file, because that blocks theming.
clip-path with polygon() takes straight-edged points, so rounded corners need path() or an SVG reference. mask-image takes an image instead, which means rounded stars work and a gradient background shows through the star shape.
Plotting polygon points by hand is slow. A clip-path generator will output them from a visual shape.
On support, Chrome 120 (5 December 2023) was the last of the three engines to ship unprefixed mask, which is why Baseline dates the property to December 2023 (web-features project).
Font Awesome and Bootstrap Icons ship a star, and so does Material Symbols. Still, loading a font file for one glyph costs a request that inline SVG avoids.
Sizing stars with em units
Set font-size on the wrapper and size every star in em, so one declaration scales the whole row.
Unicode glyphs and icon fonts scale with font-size directly. Inline SVG needs a width and height of 1em, and a mask needs that same 1em on both mask-size and the element itself.
Em-based stars also follow the visitor’s text size settings.
Setting fill and empty colors with custom properties
Store both colors on the wrapper as –star-fill and –star-empty, and every technique reads them. A theme change then touches two lines.
Amber (#f5a524) for filled and light gray (#d4d4dc) for empty is the pair in the theplusaddons example. Light gray on a white background is low contrast, so check the empty color before shipping.
How to show a fractional rating like 3.7 out of 5
See the Pen
Percentage Based Star Rating With Font Awesome by Filcp (@filcp)
on CodePen.
Divide the rating by the maximum, multiply by 100, and fill that share of the star row. For 3.7 out of 5 the fill is 74%, held in one custom property that whichever technique draws the stars reads.
The theplusaddons guide (2026) runs the same arithmetic. A 3.5 out of 5 fills 70%, and 4.3 fills 86%.
With Unicode stars, a linear-gradient with a hard stop at calc(var(–rating) / 5 * 100%) runs across the row. Add background-clip: text and a transparent text color, and the gradient gets cut to the shape of the glyphs.
Inline SVG works differently. The row of stars sits under an overlay whose width is the unfilled share (100% minus the fill), tinted with mix-blend-mode so the page background stays untouched.
A mask splits the job another way, with mask-image supplying the star tiles and a gradient background supplying the fill.
Hard-stop gradients are fiddly to write from scratch, and a gradient generator shows the stop positions before you copy them.
The “Fractional SVG stars with CSS” write-up (updated December 2021) documents a switch from stacked PNG overlays, which caused extra requests and CLS issues, to inline SVG with one computed overlay width.
Component libraries land in the same place. HeroUI Pro’s Rating exposes a –rating-partial variable for the width of its partial overlay, and Reka UI’s Rating exposes –reka-rating-item-step-width to clip each fractional step (both per their documentation). Whatever the tool, the number underneath is a percentage held in a custom property.
One thing breaks this. The gradient spans the whole row, gaps included, so unless the gap is zero (put the spacing inside each star as padding), 74% of the row is not 74% of the stars.
MDN also advises an @supports test for background-clip: text, with an accessible alternative where it fails, because the effect disappears on opaque text.
How to build a clickable CSS star rating without JavaScript
See the Pen
CSS Star Ratings with Radio Buttons by Lenny Sirivong (@lsirivong)
on CodePen.
Five radio inputs sharing one name do most of the work. Hide the inputs visually, style each label as a star, and let :checked and :has() light them up. Selection, keyboard movement and form submission all come from the radio group, so there is nothing to wire up.
- Wrap five radio inputs in a fieldset with a legend, give them one shared name, and put each input before its label in natural order, 1 to 5.
- Hide each input with a visually-hidden rule, not display: none, which removes the radio from the tab order and the accessibility tree (Ben Myers, Encyclopedia of Accessibility).
- Style each label as a star in the empty color, using a Unicode character or a mask.
- Add the fill color through two separate rules: input:checked + label, and label:has(~ input:checked).
- Add the hover preview through label:hover and label:has(~ label:hover), and reset the checked fill while the pointer is over the group.
Reversed order with row-reverse
Sibling combinators only look forward, so older builds list the highest rating first in the markup (Lea Verou, 2011) and flip the row on screen, often with flex-direction: row-reverse.
The cost shows up on the keyboard. A DEV Community write-up found that with row-reverse the arrow keys follow DOM order, not screen order, so Right Arrow lowers the rating. That is a bad surprise for anyone navigating without a mouse.
Natural order with :has()
With :has(), the inputs stay in natural order and label:has(~ input:checked) lights every label before the checked input.
Support starts at Chrome 105, Safari 15.4 and Firefox 121 (caniuse). The selector is Baseline Newly available since December 2023 (MDN), and Widely available as of the June 2026 Baseline digest (web.dev).
A browser without :has() drops the entire rule that contains it, which is why the :has() rule sits apart from the checked rule. With nothing checked yet, all five stars stay in the empty color.
Like most CSS hover effects, the preview has to override a state that already exists. While the pointer is over the group, the checked color resets to the empty color so the hovered stars win.
Range input as a one-element option
A native range input turns into a star rating with a mask and a few sizing lines, and Temani Afif’s CSS-Tip version reads the star count from the max attribute. Setting min=”.5″, step=”.5″ and max=”5″ gives half stars, which a radio group cannot do without doubling its inputs.
A styled range slider still needs vendor pseudo-elements for the thumb, ::-webkit-slider-thumb in Chrome, Edge and Safari and ::-moz-range-thumb in Firefox. MDN lists both as non-standard.
Alvaro Montoro’s single-element version also keeps one inline JavaScript command, so it is not fully script-free.
Radios are the better pick when the rating goes into a form. If half stars matter more to you than markup semantics, go with the range input.
How to make a CSS star rating accessible
A display rating needs a text name, and an input rating needs labeled radio inputs. Both need a visible focus ring, stars with 3:1 contrast and a 24 by 24 pixel hit area. These fail independently, so a good-looking widget can still lock people out.
For a display rating, a span with role=”img” and an aria-label of “Rating: 4 out of 5 stars” is the exact pattern MDN’s img role page shows. It is ARIA doing a job no native element is built for, since HTML has no star rating element.
Descendants of role img are presentational, so the star glyphs inside stay silent and the label carries the value.
Material UI’s Rating component is a useful reference for the input side. It renders six radio buttons (one per star, plus a zero that is checked by default) with text labels such as “1 Star” and “2 Stars”, and keyboard behavior stays native (MUI documentation).
Wrap the group in a fieldset with a legend that asks the question. Keep each label’s text in the DOM, visually hidden, so every star announces as “3 stars” and not as an unnamed radio.
For focus, draw the ring on the label with input:focus-visible + label. W3C lists a removed or invisible focus indicator as failure F78 against SC 1.4.11, 2.4.7 and 2.4.13, and :focus-visible is Baseline Widely available (MDN).
W3C’s Understanding document for SC 1.4.11 uses a star rating as its failing example. Pale yellow (#FFF000) stars on white measure 1.2:1 against a required 3:1. A second example in the same document, stars outlined in black and filled yellow or white, fails SC 1.4.1 (Use of Color) rather than 1.4.11, because hue alone carries the value.
Passing versions use a solid fill against an outline, or a thicker border plus the yellow fill. Material UI defaults to color plus shape for the same reason.
A gradient-masked star has only color to work with, so its empty color has to clear 3:1 on its own. A quick color contrast check on the filled and empty pair takes seconds.
The hit area is where small stars get caught. SC 2.5.8 sets 24 by 24 CSS pixels at Level AA, and a star sized at 1em on a 16 pixel font is only 16 by 16. Padding on the label grows the target without growing the glyph.
Undersized targets still pass if a 24 CSS pixel diameter circle centered on each one does not intersect another target or the circle of another undersized target (W3C).
The thresholds, for reference:
- 3:1 minimum contrast for the visual information that identifies a control and its states (WCAG 2.2 SC 1.4.11, Level AA)
- 24 by 24 CSS pixels minimum target size (SC 2.5.8, Level AA)
- 44 by 44 CSS pixels enhanced target size (SC 2.5.5, Level AAA)
- 1.2:1 contrast of #FFF000 yellow on white, listed by W3C as a failing star rating
How to pair a CSS star rating with AggregateRating markup
CSS stars never reach search results by themselves. Google reads AggregateRating structured data, which needs a ratingValue plus a ratingCount or reviewCount, nested in a supported type such as Product (Google Search Central, documentation updated 8 September 2026).
The name of the item being rated is required too. bestRating and worstRating are only recommended, and Google assumes 5 and 1 when they are left out. Decimals take a dot, never a comma, so 4.4 and not 4,4.
CSS takes a dot as well, so one number can feed both the –rating custom property and the JSON-LD ratingValue. In Microdata and RDFa, a content attribute lets the visible text read 4,4 while the markup carries 4.4.
Google’s own Product example pairs a ratingValue of 4.4 with a ratingCount of 89. The matching CSS fill is 88%.
In a typical product card design, the stars, the average and the count sit together. Google requires that users can see the aggregate rating on the page, so print the same numbers the markup carries.
The markup has limits, and a few of them catch people off guard:
- Only supported types qualify. Google names Book, Course, Event, LocalBusiness, Movie, Product, Recipe and SoftwareApplication, plus a few others such as Game, Episode and MediaObject. Article is not among them, so a rating widget on a blog post earns no stars
- Ratings collected on other websites cannot be aggregated into yours
- LocalBusiness and Organization pages are ineligible when the reviewed entity controls the reviews about itself, a guideline Google introduced in September 2019
- Valid markup carries no guarantee that a rich result appears
When does a CSS star rating stop working?
The limits show up when the vote has to leave the page without a form submit, when the layout flips right-to-left, when half stars are needed from radio inputs, or when the browser forces its own colors. Each one is documented.
A radio group submits through a form or it does not submit. MDN confirms that only checked radios are sent, and a group with nothing selected is left out of the form data entirely.
Registering a vote without leaving the page still takes a script. A plain form post works, at the cost of a full page load.
Half-star input splits each star into two click targets, as Mishka UI’s Rating documentation puts it. With SC 2.5.8 asking for 24 CSS pixels, each half needs 24, so a star has to be 48 pixels wide unless the spacing exception applies.
In a right-to-left layout, MDN notes that row and row-reverse follow the container’s directionality, so a row starts on the right. The row-reverse build then shows its stars in the opposite order, and a gradient written with a physical angle keeps filling from the left.
Decide whether the stars mirror, then match the gradient angle to that choice.
Forced-colors mode is the least obvious one. MDN lists background-image among the properties it rewrites, so any value that is not url-based becomes none. Color, border-color and SVG fill are forced to system colors as well.
Gradient-filled stars lose their fill, while a filled glyph and an outline glyph keep their shape difference. The feature has been available across browsers since September 2022 and is Baseline Widely available, so a small @media (forced-colors: active) rule with system color keywords is a safe patch.
Past these limits, stop adding selectors. Web Awesome’s rating component documents a precision attribute for half stars, a name and value submitted with the form, and readonly and required states (Web Awesome documentation).
The price is a JavaScript dependency. For a five-star review form with full-star votes, a native radio group styled with CSS still beats any div-based widget.
CSS Star Rating Examples To Check Out
Half Star Rating CSS
See the Pen
CSS Half Star Rating by mirko (@mrk1989)
on CodePen.
Pure CSS Star Rating Widget
See the Pen
Pure CSS Star Rating Widget by James Barnett (@jamesbarnett)
on CodePen.
Emoticon Star Rating
See the Pen
Emoticon Star Rating by Vineeth.TR (@vineethtrv)
on CodePen.
Pure CSS 5-Star Rating
See the Pen
Pure CSS 5-Star Rating by Andrea Crawford (@andreacrawford)
on CodePen.
Feedback Rating Disputelab
See the Pen
Feedback Rating DisputeLab by Ryan Parag (@ryanparag)
on CodePen.
Simple Star Rating CSS
See the Pen
simple star rating by Mert Cukuren (@knyttneve)
on CodePen.
Rate Star Buttons (Pure CSS)
See the Pen
Rate star buttons (pure css) by Valeriia (@valerite-dev)
on CodePen.
Book Store UI with Star Rating
See the Pen
Book Store UI by Aysenur Turk (@TurkAysenur)
on CodePen.
Ratings Inspiration
See the Pen
Ratings Inspiration by Tadaima (@tadaima)
on CodePen.
Apple AppStore Styled Star Rating
See the Pen
Apple AppStore styled star rating in pure HTML/CSS by Adrian Lambertz (@adrianlambertz)
on CodePen.
Rating System (CSS Only)
See the Pen
Rating System (CSS Only) by Elton Kamami (@eltonkamami)
on CodePen.
Movie Card UI With Pure CSS3 Animation Rating
See the Pen
Movie Card Interactive UI With Pure CSS3 Animation Rating by Reece McDonald (@rmcdonalddesigns)
on CodePen.
CSS Star Rating With Counter
See the Pen
CSS Star Rating with counter by 大漠 (@airen)
on CodePen.
Star Ratings Survey Form
See the Pen
Star ratings survey form by Ron Gilmour (@rgilmour)
on CodePen.
Stars & Heart Rating
See the Pen
stars & heart rating by Crystal (@s_crystal)
on CodePen.
Flat CSS3 Rating System
See the Pen
Flat CSS3 rating system by Renaud Tertrais (@renaudtertrais)
on CodePen.
Stars Rating with Smile Animation
See the Pen
Stars rating animation by Aaron Iker (@aaroniker)
on CodePen.
Full Css3 Only Colorful Star Ratings \w Inputs
See the Pen
Full css3 only colorful star ratings \w inputs by daniesy (@daniesy)
on CodePen.
Star Rating By Michael
See the Pen
Star Rating by Michael Trythall (@mtrythall)
on CodePen.
Star Rating Widget
See the Pen
Star rating widget by Marian Alexandru (@marianissimus)
on CodePen.
Pure CSS Rating with Reset Button
See the Pen
Rating in pure HTML5/CSS3 by Khoné Vongsouthi (@khone-vongsouthi)
on CodePen.
Star Rating in Pure CSS
See the Pen
Star Rating in Pure CSS by Christopher Kirk-Nielsen (@chriskirknielsen)
on CodePen.
How Are You?
See the Pen
How Are You? by Ryan Mulligan (@hexagoncircle)
on CodePen.
Half-Star Rating Bharlotte Dann
See the Pen
Half-Star Rating by Charlotte Dann (@pouretrebelle)
on CodePen.
WooCommerce CSS SVG Star Rating
See the Pen
WooCommerce CSS SVG Star Rating by Jan Wagner (@janwagner)
on CodePen.
Stand Up Rating Animation
See the Pen
Star rating by Giel Berkers (@kanduvisla)
on CodePen.
Standalone SVG CSS-only Star Rating Component
See the Pen
Standalone SVG CSS-only star rating component by Damián Muti (@damianmuti)
on CodePen.
Stars By Jordan-Simonds
See the Pen
stars by Jordan-Simonds (@jexordexan)
on CodePen.
Rating with Smooth Fade
See the Pen
CSS-only Star Rating by Marcus Burnette (@mburnette)
on CodePen.
Alien Rating Control
See the Pen
Alien Rating Control by Darin (@dsenneff)
on CodePen.
CSS Star Rating FAQ
Can Font Awesome or Bootstrap Icons replace CSS-drawn stars?
See the Pen
star rating with font icons by Brandon Kennedy (@brandonkennedy)
on CodePen.
Yes. An icon font or SVG icon set supplies the star shape, and CSS still controls size, color and state.
Fractional fills need an overlay or mask on top, and the icons should be hidden from screen readers so the rating label does the talking.
How do I animate stars on hover?
See the Pen
Star Rating Animation by Roxy (@roxy225)
on CodePen.
Add a transition to the label, such as a short transform scale or color change, and trigger it on :hover.
Wrap the motion in prefers-reduced-motion: no-preference so people who turned animation off in their system settings do not get it.
Can I build a star rating with Tailwind CSS?
Yes. Utility classes handle size, color and spacing, and the peer-checked variant handles state when the row is reversed.
For natural DOM order, the has- variant does the same job as the :has() selector in plain CSS.
How do I change the number of stars, for example to 10?
See the Pen
Pure CSS Star Rating from 0 to 8 with colored points of the star by Jan Wagner (@janwagner)
on CodePen.
Add radio and label pairs until you reach ten, then change the divisor in the fill calculation to match, such as calc(var(–rating) / 10 * 100%).
Set bestRating to 10 in the AggregateRating markup too, because Google assumes 5 when it is missing.
How do I save the selected rating to a server?
See the Pen
Interactive Customer Feedback Form (For a pet store) by Jamie Coulter (@jcoulterdesign)
on CodePen.
Put the radios inside a form with a shared name and a value on each input. On submit, the checked radio is sent as name=value (MDN).
The page reloads unless JavaScript intercepts the submit and sends it with fetch. CSS cannot send data on its own.
Which CSS Star Rating Approach to Ship First
Ship the read-only display first. That means a CSS star rating driven by one custom property, with a text label and AggregateRating markup.
After that comes the radio input, with its focus ring and 24 pixel targets, and last the half-star, right-to-left and forced-colors checks.
Display goes first because the contrast and markup rules already apply to it, and later steps reuse its stars.
The cost is a first release that collects no votes, so scores must arrive from an existing review system. Half-star input raises the bar further, since five stars at 48 pixels each need 240 CSS pixels of row width before any gaps.
The order was checked on 14 October 2026 and changes if votes must save without a page reload, which adds JavaScript or a component at step two.
Run the finished widget through a web accessibility checklist before release.
- What is Backend in Web Development? - September 13, 2026
- What is Frontend Development? - September 11, 2026
- How to Make a Button in Figma: Design Best Practices - September 10, 2026


