Playground
A select for one or several values:
- Plain: pick from a list.
searchable: type to filter the options in the browser.remote: type to search on the server. It follows the same pattern as the tree: the select asks with an event, and the server answers withresults.
The live value is the value property (a string, or an array with multiple), so data-bind works.
Examples
Autocomplete from the server
Type a star, planet or moon ("or", "sat", "eu"…), from the site's example dataset:
- After a short pause, the select emits
sb-searchwith the query. @get('/demo/data/search?q=…&into=_found')asks the server.- The server patches
$_foundwith the results. data-attr:resultshands them back.
data-indicator shows the spinner while the request is in flight.
<div data-signals="{_found: [], _body: '', _searching: false}" style="display: grid; gap: 12px">
<sb-select remote clearable label="Find a star, planet or moon" placeholder="Type a name…"
data-attr:results="JSON.stringify($_found)"
data-attr:loading="$_searching"
data-preserve-attr="results loading"
data-indicator:_searching
data-on:sb-search="@get('/demo/data/search?kind=star,planet,dwarf,moon&into=_found&delay=150&q=' + encodeURIComponent(evt.detail.query))"
data-bind:_body__prop.value></sb-select>
<span>Picked: <b data-text="$_body || 'nothing yet'"></b></span>
</div>
The server side is a plain Datastar handler (Go here; any language works):
func search(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query().Get("q")
datastar.NewSSE(w, r).MarshalAndPatchSignals(map[string]any{
"_found": find(q), // [{value, label, description?}], at most a few
})
}
Or the server re-renders the element with a new results attribute: a changed attribute always wins.
Searchable
<sb-select searchable label="Planet" placeholder="Type to filter"
options='[{"value":"mercury","label":"Mercury","description":"0.39 AU"},{"value":"venus","label":"Venus","description":"0.72 AU"},{"value":"earth","label":"Earth","description":"1 AU"},{"value":"mars","label":"Mars","description":"1.52 AU"},{"value":"jupiter","label":"Jupiter","description":"5.2 AU"},{"value":"saturn","label":"Saturn","description":"9.5 AU"},{"value":"pluto","label":"Pluto","description":"No longer a planet","disabled":true}]'></sb-select>
Several values
With multiple, the value is an array. Keep it in a signal with sb-change: data-bind treats array signals as checkbox groups, which is a different thing.
<div data-signals="{_crew: ['Ada', 'Yuri']}" style="display: grid; gap: 12px">
<sb-select multiple searchable clearable label="Crew" placeholder="Add crew"
options='["Ada","Buzz","Chris","Mae","Sally","Valentina","Yuri"]' value='["Ada","Yuri"]'
data-on:sb-change="$_crew = evt.detail.value"></sb-select>
<span>Crew: <b data-text="$_crew.join(', ') || 'nobody'"></b></span>
</div>
With commands
Give it a name, and it emits sb-change with { name, value } when the value changes: ready to post as a command. With confirm, it sets :state(pending) until the server's re-rendered value matches, and revert() goes back to the server's value when a command is rejected. See Commands and components.
Options
options (and results, for remote searches) is a JSON array of strings, or of {value, label?, description?, disabled?}. The server can change either at any time. A selected value keeps its label even after the options it came from are gone.
Accessibility
It follows the ARIA combobox pattern:
- Structure: the input is a
comboboxcontrolling alistbox, with the highlighted option inaria-activedescendant. - Keys:
- Down and Up open the list and move through it; Home and End jump.
- Enter picks, and Escape closes.
- Backspace in an empty input removes the last chip, with
multiple.
- Loading: the input is
aria-busywhile results are on their way.
The list is a native popover, so it is never clipped by a scrolling container.
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 added later by a Datastar morph.
<!-- Once per page: Datastar with Rocket, and the Starbase autoloader.
It loads every <sb-…> component the first time its tag appears. -->
<script type="importmap">
{ "imports": { "datastar": "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar-rocket.js" } }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/autoloader.js"></script>
<!-- Optional, no flash of undefined elements: class="sb-cloak" on <html>, and -->
<style>.sb-cloak :not(:defined) { visibility: hidden }</style>
<sb-select label="Destination" placeholder="Pick a planet" value="Mars" options='["Mercury","Venus","Earth","Mars","Jupiter","Saturn"]' style="inline-size: 14rem"></sb-select>
<!-- In production, pin today's catalog instead of the latest: the browser then
refuses any file that changed. Add "integrity" to the import map above: the
hashes from https://starbase.zweiundeins.gmbh/c/@0a1ad1bf98a4/importmap.json and Datastar's, below. -->
<!--
<script type="importmap">
{ "imports": { "datastar": "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar-rocket.js" },
"integrity": { "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar-rocket.js": "sha384-vUxZojLrF1Ar3de5h7VINqhJXBgjyZS4U49pHvGa87kum6j5Xn6JRziuARNw5ELG", "…": "…from importmap.json" } }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/@0a1ad1bf98a4/autoloader.js" integrity="sha384-IvSCUjEWPS+wwXunU+48x96KmyWDx/Y4LeCwCL8cWFqJ/zHmTsTOsSMrzTOcpW3D"></script>
-->
<!-- Or load just this component, pinned to this version: -->
<!-- <script type="module" src="https://starbase.zweiundeins.gmbh/c/select@b9f1cf4809e3/select.js" integrity="sha384-HVn6ouP86ed76nVZET8iGK+YLjjLCfceaLslH2od/v+LWzbvm49RB19NwzTaRJZB"></script> -->Size
Each file compressed on its own, the way it is served (gzip -9, brotli -11). Datastar and Rocket are shared by every component and not counted.
| File | Original | gzip | brotli |
|---|---|---|---|
select.js | 17.6 kB | 6.0 kB | 5.2 kB |
API reference
Props
| Attribute | Type | Default | Description |
|---|---|---|---|
options | json | [] | Choices: ["A", "B"] or [{value, label, description?, disabled?}]. |
results | json | [] | Remote: the results of the current search, in the same shape. The server sets it (a signal patch through data-attr, or a morph). |
value | string | "" | The value; for multiple, a JSON array or values separated by commas. A new value from the server replaces it; the live value is the value property. |
label | string | "" | Visible label. |
placeholder | string | "" | Placeholder text. |
multiple | boolean | false | Pick several; they show as chips. |
searchable | boolean | false | Type to filter the options (in the browser). |
remote | boolean | false | Type to search on the server: emits sb-search; the server answers with results. |
delay | number | 250 | Remote: debounce before sb-search, in ms. |
min-chars | number | 1 | Remote: characters needed before searching. |
loading | boolean | false | Show that results are on their way (bind it to data-indicator). |
clearable | boolean | false | Show a button that clears the value. |
disabled | boolean | false | Disable the control. |
name | string | "" | Name reported in sb-change (e.g. the field of a command). |
confirm | boolean | false | Server-confirmed value: :state(pending) while the local value differs from the server's value attribute (see revert()). |
Events
| Name | Description |
|---|---|
sb-search | Remote: the query changed (debounced). detail: { query }. Answer by setting results. |
change | The value changed. |
sb-change | The value changed. detail: { name, value } (a string, or an array for multiple): ready for a command. |