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.

Where is web design headed next?

Discover the latest web design statistics: industry growth, design trends, technology adoption, and insights defining the future of the web.

Explore the Data →
MethodColor and scalingFractional fillFails when
Unicode (U+2605, U+2606)Follows color and font-sizeNeeds gradient or overlayEach font draws its own star
Inline SVGExact shape, fill follows currentColorOverlay or stacked copiesFill is hard-coded in the file
clip-path or mask-imageAny background shows throughGradient behind the maskOlder Chrome needs -webkit-mask
Icon fontFollows color and font-sizeSame limits as UnicodeExtra 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.

  1. 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.
  2. 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).
  3. Style each label as a star in the empty color, using a Unicode character or a mask.
  4. Add the fill color through two separate rules: input:checked + label, and label:has(~ input:checked).
  5. 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.

Bogdan Sandu
Latest posts by Bogdan Sandu (see all)