Navigation

v0.0.4
Forms

Rating

A score out of a few stars, as native radios.

On this page

A rating is a score out of a few stars. It is a group of native radios, each drawn as a star in ink, so the arrow keys, the form value and assistive technology work as they do for any radio group.

Rating

<fieldset class="rating" aria-label="Rating">  <input type="radio" class="rating-input" name="rating" value="1" aria-label="1 star">  <input type="radio" class="rating-input" name="rating" value="2" aria-label="2 stars">  <input type="radio" class="rating-input" name="rating" value="3" aria-label="3 stars">  <input type="radio" class="rating-input" name="rating" value="4" aria-label="4 stars" checked>  <input type="radio" class="rating-input" name="rating" value="5" aria-label="5 stars"></fieldset>
Hover to preview a score, click to set it.

#How the stars fill

The radios are in reading order, one to five. :has() fills every star that has the checked one after it, and the checked one itself: .rating-input:has(~ .rating-input:checked). The same selector with :hover previews the score under the pointer. Before :has(), this needed the stars in reverse order and a reversed row, which broke the keys.

#Sizes

Stars are 20px. rating-sm is 16px, for a row in a list, and rating-lg is 24px. On a touch screen each star is drawn at 28px at least, and a star that can be chosen stands in a 44px target of its own: the stars part instead of swelling. Stars that only show a score keep their spacing.

Sizes

<fieldset class="rating rating-sm" aria-label="Small rating">  <input type="radio" class="rating-input" name="rating-sm" aria-label="1 star">  <input type="radio" class="rating-input" name="rating-sm" aria-label="2 stars" checked>  <input type="radio" class="rating-input" name="rating-sm" aria-label="3 stars">  <input type="radio" class="rating-input" name="rating-sm" aria-label="4 stars">  <input type="radio" class="rating-input" name="rating-sm" aria-label="5 stars"></fieldset><fieldset class="rating rating-lg" aria-label="Large rating">  <input type="radio" class="rating-input" name="rating-lg" aria-label="1 star">  <input type="radio" class="rating-input" name="rating-lg" aria-label="2 stars">  <input type="radio" class="rating-input" name="rating-lg" aria-label="3 stars">  <input type="radio" class="rating-input" name="rating-lg" aria-label="4 stars">  <input type="radio" class="rating-input" name="rating-lg" aria-label="5 stars" checked></fieldset>

#Read only

To show a score, disable the radios: the stars keep their ink and stop answering the pointer. Say the score in words beside them, since a row of stars is not a number.

Read only

<div class="flex items-center gap-xs text-sm fg-text-muted"><fieldset class="rating rating-sm" aria-label="Rated 3 out of 5" disabled>  <input type="radio" class="rating-input" name="rating-ro" aria-label="1 star">  <input type="radio" class="rating-input" name="rating-ro" aria-label="2 stars">  <input type="radio" class="rating-input" name="rating-ro" aria-label="3 stars" checked>  <input type="radio" class="rating-input" name="rating-ro" aria-label="4 stars">  <input type="radio" class="rating-input" name="rating-ro" aria-label="5 stars"></fieldset>3.0 from 128 reviews</div>

#Accessibility

  • Name the group with aria-label or a <legend>, and each radio with its score.
  • A star is cut with a mask, which would also cut its own focus ring, so the group carries the ring while one of its radios has keyboard focus.
  • Stars are ink, not yellow: the interface keeps hue for status.

#Classes

ClassDescription
ratingThe group of stars, on a fieldset named by aria-label or a legend. It carries the focus ring for its radios
rating-inputA star, on a radio named with its score. The radios stand in reading order, lowest first
rating-sm16px stars, for a row in a list; the default is 20px. With rating
rating-lg24px stars; the default is 20px. With rating

#Source

@master/ui/components/rating.css
/* * Rating: a score out of a few stars, as a group of native radios drawn as stars in ink. * :has() fills every star up to the checked one, and up to the one under the pointer as a preview, so the * markup is five radios in reading order with no reversed rows and no script. */@layer components {  .rating {    --rating-size: 1.25rem;    display: inline-flex;    gap: .125rem;    min-width: 0;    margin: 0;    padding: 0;    border: 0;    border-radius: calc((var(--radius-chip) - 2px) * var(--corner-scale));    corner-shape: var(--corner-shape);    vertical-align: middle;    /* A star is cut with a mask, which would also cut its own focus ring: the group carries the ring. */    &:has(.rating-input:focus-visible) {      @apply --focus-ring;      outline-offset: 4px;    }    &.rating-sm { --rating-size: 1rem; }    &.rating-lg { --rating-size: 1.5rem; }    /* The 44px targets of a touch screen stand edge to edge. */    @media (pointer: coarse) {      &:has(.rating-input:not(:disabled)) {        gap: 0;      }    }  }  .rating-input {    flex: none;    width: var(--rating-size);    height: var(--rating-size);    margin: 0;    padding: 0;    border: 0;    outline: none;    background: var(--color-line-strong);    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2.6c.4 0 .8.2 1 .6l2.5 5.2 5.7.8c.9.1 1.3 1.3.6 1.9l-4.1 4 1 5.7c.2.9-.8 1.6-1.6 1.2L12 19.3l-5.1 2.7c-.8.4-1.8-.3-1.6-1.2l1-5.7-4.1-4c-.7-.6-.3-1.8.6-1.9l5.7-.8L11 3.2c.2-.4.6-.6 1-.6Z'/%3E%3C/svg%3E") center / contain no-repeat;    appearance: none;    cursor: pointer;    touch-action: manipulation;    -webkit-tap-highlight-color: transparent;    transition: background-color var(--duration-quick) var(--easing-glide), scale var(--duration-quick) var(--easing-glide);    /* Filled: the checked star and every star before it. */    &:is(:checked, :has(~ .rating-input:checked)) {      background: var(--color-text-strong);    }    &:active:not(:disabled) {      scale: .88;      transition-duration: var(--duration-press);    }    &:disabled {      cursor: default;    }    /*     * On a touch screen a star is drawn at 28px at least, and one that can be chosen stands in a 44px target:     * the mask keeps the star's size and the box around it grows, so the stars part instead of swelling.     */    @media (pointer: coarse) {      width: max(var(--rating-size), 1.75rem);      height: max(var(--rating-size), 1.75rem);      &:not(:disabled) {        width: 2.75rem;        height: 2.75rem;        mask-size: max(var(--rating-size), 1.75rem);      }    }    @media (prefers-reduced-motion: reduce) {      &:active:not(:disabled) {        scale: none;      }    }    @media (forced-colors: active) {      background: GrayText;      forced-color-adjust: none;      &:is(:checked, :has(~ .rating-input:checked)) {        background: Highlight;      }    }  }  /* Under the pointer the stars preview the score it would set: filled up to the pointer, empty after it. */  @media (hover: hover) {    .rating:has(.rating-input:hover:not(:disabled)) .rating-input {      background: var(--color-line-strong);      &:is(:hover, :has(~ .rating-input:hover)) {        background: var(--color-text-strong);      }    }  }}