Playground
A row of pixel hearts (or stars) for a score. Point to preview, click to pick (on touch, tap or drag along the row), or use the arrow keys. The live value is the value property, so data-bind works, and a new value attribute from the server replaces it, including value="0" to clear it.
Examples
Bound to a signal
<div data-signals="{_score: 3}" style="display: grid; gap: 12px">
<sb-rating label="How was the launch?" data-bind:_score__prop.value></sb-rating>
<span>Score: <b data-text="$_score"></b></span>
</div>
Half steps and stars
<div style="display: grid; gap: 12px">
<sb-rating icon="star" precision="0.5" value="2.5"></sb-rating>
<sb-rating icon="star" precision="0.5" value="4" max="10" size="sm"></sb-rating>
</div>
Read only
For showing a score, e.g. an average: readonly keeps the value ("4.5 of 5") for screen readers, and takes it out of the tab order.
<sb-rating readonly value="4.5" precision="0.5" size="sm"></sb-rating>
Clearable
With clearable, picking the current value again resets it to 0. Only picking clears: an arrow key or End at the maximum leaves it there.
<sb-rating clearable value="2"></sb-rating>
With commands
Give it a name, and it emits sb-change with { name, value } when a value is picked: ready to post as a command. With confirm, it sets :state(pending) until the server's re-rendered attribute matches, and revert() goes back to the server's value when a command is rejected. See Commands and components.
Forms
Inside a <form>, sb-rating submits its value under its name (name=3.5, name=0 when unrated), new FormData(form) and Datastar's contentType: 'form' include it, and a form reset brings back the server's value. It is not a form-associated element yet (Rocket can't declare one), so required and validity, <fieldset disabled>, <label for> and the form attribute don't reach it. With commands, sb-change carries { name, value } (see With commands).
Styling
Style it from your page's CSS, without changing the component or importing anything into it. Custom properties, inherited properties and ::part() all reach into its shadow root.
- Size:
size(sm,md,lg) sets the icons to1rem,1.5remor2.25rem. For another size, setinline-sizeandblock-sizeon::part(unit). - Fonts: the label uses your page's font.
- Colours:
--sb-rating-colorfills the icons (by default--sb-dangerfor hearts,--sb-warnfor stars) and--sb-rating-shineis their highlight; empty icons are--sb-border-strong. The label is--sb-text-2, the focus ring--sb-brand-light. - Parts:
label,base(the row of icons) andunit(each icon). Your page's::part()rules win over the component's own, without!important.
<style>
.my-rating { --sb-rating-color: var(--sb-brand-light); }
.my-rating::part(unit) { inline-size: 2rem; block-size: 2rem; }
</style>
<sb-rating class="my-rating" icon="star" label="Crew comfort" value="4"></sb-rating>
Accessibility
The row is a slider (Arrow keys change the value by one step, Home clears it, End fills it), named by label or "Rating", with aria-valuetext like "3 of 5". Read-only ratings are a slider with aria-readonly, outside the tab order, with the same value and text.
Right to left (dir="rtl"), the row starts at the right: half units fill from the right, and Left and Right follow the row (Left raises the value), like a native range input.
Installation
Add Datastar with Rocket and the Starbase autoloader once per page, then use the tag: the autoloader imports each component the first time its tag appears, including tags a Datastar morph adds later. The Datastar here is v1.0.4 with Starbase's fixes until they are released; the components also run on the official release.
<script type="importmap">
{ "imports": { "datastar": "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js" } }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/autoloader.js"></script>
<sb-rating value="3.5" precision="0.5" size="lg"></sb-rating>No flash of undefined elements: put class="sb-cloak" on <html> and add .sb-cloak :not(:defined) { visibility: hidden } to your CSS. The autoloader removes the class once the page's components are defined.
Load just this component, pinned to this version: all its modules in one minified file with its integrity hash, so the browser refuses it if a single byte changes.
<script type="importmap">
{
"imports": { "datastar": "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js" },
"integrity": {
"https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js": "sha384-mvTnynHaljKNMleD0bj6/rWROnluHlNmbc5rOQTd+fQH3YBDYhlvHdNquEX99by5"
}
}
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/rating@5d21008de7e4/rating.bundle.min.js" integrity="sha384-OnFkYYE97sZ/0v3qD9boNF0a3Vy3a7vBPzxWN9Hf8oAi1P4jJ2WapaLEdu4XtJWM"></script>
<sb-rating value="3.5" precision="0.5" size="lg"></sb-rating>Or the minified module files (one file, which the browser requests as it finds their imports), each pinned in the import map:
<script type="importmap">
{
"imports": { "datastar": "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js" },
"integrity": {
"https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js": "sha384-mvTnynHaljKNMleD0bj6/rWROnluHlNmbc5rOQTd+fQH3YBDYhlvHdNquEX99by5",
"https://starbase.zweiundeins.gmbh/c/rating@5d21008de7e4/rating.min.js": "sha384-Wx1uyR5QnhM8hnqGXyvI0fU6B0Nk2k0ou9khOyZvBp4XuoKEPkElRWrJzeftaa4s"
}
}
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/rating@5d21008de7e4/rating.min.js" integrity="sha384-Wx1uyR5QnhM8hnqGXyvI0fU6B0Nk2k0ou9khOyZvBp4XuoKEPkElRWrJzeftaa4s"></script>
<sb-rating value="3.5" precision="0.5" size="lg"></sb-rating>In production, pin today's whole catalog: its autoloader and an import map with integrity hashes, so the browser refuses any file that changed. This map covers Datastar and this component; importmap.json has the hashes of every component, to merge into yours.
<script type="importmap">
{
"imports": { "datastar": "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js" },
"integrity": {
"https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js": "sha384-mvTnynHaljKNMleD0bj6/rWROnluHlNmbc5rOQTd+fQH3YBDYhlvHdNquEX99by5",
"https://starbase.zweiundeins.gmbh/c/rating@5d21008de7e4/rating.bundle.min.js": "sha384-OnFkYYE97sZ/0v3qD9boNF0a3Vy3a7vBPzxWN9Hf8oAi1P4jJ2WapaLEdu4XtJWM"
}
}
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/@339249aca48f/autoloader.js" integrity="sha384-ZNpMayI1XShztIEX0iDHyCDDbhT8H+t+zEESUq4BCrN4b+7m931LQEl5x0vbY+Vo"></script>
<sb-rating value="3.5" precision="0.5" size="lg"></sb-rating>Components import only datastar and their own files, so copying is enough: save each component's one file (and a file it loads on first use, keeping its folder), and point the import map at your own copy of datastar-rocket.js (the /js/ paths stand for yours).
<sb-rating>rating.bundle.min.jsone file 2.2 kB brotli
<script type="importmap">
{ "imports": { "datastar": "/js/datastar-rocket.js" } }
</script>
<script type="module" src="/js/rating/rating.bundle.min.js"></script>
<sb-rating value="3.5" precision="0.5" size="lg"></sb-rating>Or copy the module files, keeping each component's folder, and load each component's module:
<script type="importmap">
{ "imports": { "datastar": "/js/datastar-rocket.js" } }
</script>
<script type="module" src="/js/rating/rating.min.js"></script>
<sb-rating value="3.5" precision="0.5" size="lg"></sb-rating>Size
A page loads this component as one file, its bundle, compressed the way it is served (gzip -9, brotli -11): brotli is what it downloads. Datastar and Rocket are shared by every component and not counted.
| File | Original | gzip | brotli |
|---|---|---|---|
rating.bundle.min.js | 4.9 kB | 2.4 kB | 2.2 kB |
As module files: one request, 2.1 kB minified with brotli
For a page that loads the module files as they are, requesting each one as it finds its import, or bundles them itself. Each file compressed on its own; the minified one is what the page downloads, and the readable source is always there too.
| File | Original | gzip | brotli | minified |
|---|---|---|---|---|
rating.js | 11.6 kB | 4.8 kB | 4.1 kB | 2.1 kB |
API reference
Props
| Attribute | Type | Default | Description |
|---|---|---|---|
value | number | 0 | The value. A new value from the server replaces it; the live value is the value property. |
max | number | 5 | Number of hearts (or stars). |
precision | "1" | "0.5" | "1" | Step: whole or half units. |
icon | "heart" | "star" | "heart" | Pixel sprite. |
size | "sm" | "md" | "lg" | "md" | Size. |
label | string | "" | Visible label (also the accessible name). |
readonly | boolean | false | Show the value; no interaction. |
clearable | boolean | false | Picking the current value again clears it to 0. |
disabled | boolean | false | Disable interaction. |
confirm | boolean | false | Server-confirmed value: :state(pending) while the local value differs from the server's value attribute (see revert()). |
name | string | "" | Name reported in sb-change (e.g. the field of a command) and submitted with a form. |
Events
| Name | Description |
|---|---|
change | When the value is committed. |
sb-change | Same moment. detail: { name, value }: ready for a command. |