I always thought that the semantically appropriate way to represent a rating (e.g. a star rating) is a <meter>
element. They essentially convey the same type of information, the star rating is just a different presentation.
An example of a star rating widget, from Amazon
However, trying to style a <meter>
element to look like a star rating is …tricky at best. Not to mention that this approach won’t even work in Shadow trees (unless you include the CSS in every single shadow tree).
So, I set out to create a proper web component for star ratings. The first conundrum was, how does this relate to a <meter>
element?
- Option 1: Should it extend
<meter>
using builtin extends? - Option 2: Should it use a web component with a
<meter>
in Shadow DOM? - Option 3: Should it be an entirely separate web component that just uses a
meter
ARIA Role and related ARIA attributes?
This is what the code would look like:
<!-- Option 1 -->
<meter is="meter-discrete" max="5" value="3.5"></meter>
<!-- Options 2 & 3 -->
<meter-discrete max="5" value="3.5"></meter-discrete>
Safari has all but killed built-in extends, but there is a very small polyfill, so I didn’t mind too much. I first decided to go with that, but it turns out you can’t even mess with the Shadow DOM of the element you’re extending. You have no access to the existing Shadow DOM of the element, because it’s closed, and you cannot attach a new one. So there’s no way to add encapsulated styles, which was a strong requirement of my use case.
I did some work on Option 2, but I quickly discovered that having an internal <meter>
that everything goes through was not worth it, and it was far easier to implement it myself, with appropriate implicit ARIA through ElementInternals.
The next dilemma was even more of a conundrum: A <meter>
is not editable by default, but for a rating widget, you need it to be editable at least sometimes (e.g. see Shoelace Rating for an example). There is no established convention in HTML for elements that are readonly by default, and editable only some of the time. All editable elements we have are basically form controls that can lose editability through the readonly
attribute. For anything else, I suppose there is contentEditable
but there is no way for web components to hook into it and expose custom editing UI that overrides the one generated by the browser.
In the end what I ended up doing was creating two components:
- A
<meter-discrete>
component that is a discrete version of<meter>
- An
<nd-rating>
component that inherits from<meter-discrete>
but is editable (unlessreadonly
is specified)
I’m still unsure if this is the right way. There were a couple issues with it.
The first problem was related to encapsulation. I like to use a private #internals
property for an element’s ElementInternals
instance. However, <nd-rating>
needed to modify the internals of its parent, to add form association stuff, so I could not use a private property anymore, and you cannot attach a separate ElementInternals
object. I ended up going for a Symbol
property that the parent exports, but it still doesn’t feel like a great solution as it breaks encapsulation. Ideally JS needs protected class fields, but it doesn’t look like that’s happening anytime soon.
The other problem was related to semantics. Is it still semantically a <meter>
when it’s editable, or does it then become closer to a slider that you set by hovering instead of dragging? I decided to ignore that thought for now, but it does make me a little uneasy.
Anyhow, you can find my experiments at nudeui.com:
All NudeUI components are very much works in progress and mainly my personal experiments, but if you feel like it, please report issues in the repo. I can’t promise I’ll get to them though!