Combobox
A searchable text field with authored <option> suggestions.
Label and options
Native option elements provide the ordinary no-script option list.
Code
<link
rel="component"
href="@threadlabs/looma/components/ui-combobox/ui-combobox.html"
>
<ui-combobox
label="Destination"
disclosure
>
<option
value="north"
data-description="Harbor district"
>North terminal</option>
<option
value="west"
data-description="Riverside"
>West terminal</option>
</ui-combobox><script setup>
import { UiCombobox } from "@threadlabs/looma";
</script>
<template>
<UiCombobox
label="Destination"
:disclosure="true"
>
<option
value="north"
data-description="Harbor district"
>North terminal</option>
<option
value="west"
data-description="Riverside"
>West terminal</option>
</UiCombobox>
</template>import { UiCombobox } from "@threadlabs/looma";
export default function Example() {
return <UiCombobox
label="Destination"
disclosure
>
<option
value="north"
data-description="Harbor district"
>North terminal</option>
<option
value="west"
data-description="Riverside"
>West terminal</option>
</UiCombobox>;
}/** @jsxImportSource preact */
import { UiCombobox } from "@threadlabs/looma";
export default function Example() {
return <UiCombobox
label="Destination"
disclosure
>
<option
value="north"
data-description="Harbor district"
>North terminal</option>
<option
value="west"
data-description="Riverside"
>West terminal</option>
</UiCombobox>;
}<script>
import { UiCombobox } from "@threadlabs/looma";
</script>
<UiCombobox
label="Destination"
disclosure
>
<option
value="north"
data-description="Harbor district"
>North terminal</option>
<option
value="west"
data-description="Riverside"
>West terminal</option>
</UiCombobox>import { Component } from "@angular/core";
import { UiCombobox } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiCombobox],
template: `
<div
ui-combobox
[label]="'Destination'"
[disclosure]="true"
>
<option
value="north"
data-description="Harbor district"
>North terminal</option>
<option
value="west"
data-description="Riverside"
>West terminal</option>
</div>
`,
})
export default class Example {}Disclosure, clearable, and size
Disclosure adds the toggle, clearable adds value removal, and size controls field density.
Code
<link
rel="component"
href="@threadlabs/looma/components/ui-combobox/ui-combobox.html"
>
<ui-combobox
label="Destination"
disclosure
clearable
allow-free-text
allow-create
size="sm"
placeholder="Search…"
>
<option
value="north"
>North terminal</option>
<option
value="west"
>West terminal</option>
</ui-combobox><script setup>
import { UiCombobox } from "@threadlabs/looma";
</script>
<template>
<UiCombobox
label="Destination"
:disclosure="true"
:clearable="true"
:allow-free-text="true"
:allow-create="true"
size="sm"
placeholder="Search…"
>
<option
value="north"
>North terminal</option>
<option
value="west"
>West terminal</option>
</UiCombobox>
</template>import { UiCombobox } from "@threadlabs/looma";
export default function Example() {
return <UiCombobox
label="Destination"
disclosure
clearable
allowFreeText
allowCreate
size="sm"
placeholder="Search…"
>
<option
value="north"
>North terminal</option>
<option
value="west"
>West terminal</option>
</UiCombobox>;
}/** @jsxImportSource preact */
import { UiCombobox } from "@threadlabs/looma";
export default function Example() {
return <UiCombobox
label="Destination"
disclosure
clearable
allowFreeText
allowCreate
size="sm"
placeholder="Search…"
>
<option
value="north"
>North terminal</option>
<option
value="west"
>West terminal</option>
</UiCombobox>;
}<script>
import { UiCombobox } from "@threadlabs/looma";
</script>
<UiCombobox
label="Destination"
disclosure
clearable
allowFreeText
allowCreate
size="sm"
placeholder="Search…"
>
<option
value="north"
>North terminal</option>
<option
value="west"
>West terminal</option>
</UiCombobox>import { Component } from "@angular/core";
import { UiCombobox } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiCombobox],
template: `
<div
ui-combobox
[label]="'Destination'"
[disclosure]="true"
[clearable]="true"
[allowFreeText]="true"
[allowCreate]="true"
[size]="'sm'"
[placeholder]="'Search…'"
>
<option
value="north"
>North terminal</option>
<option
value="west"
>West terminal</option>
</div>
`,
})
export default class Example {}multiple
Multiple collects several selections in one scrolling field row. With name, a form submits one entry per chosen item's value (here teams=design, teams=docs), never the labels.
Code
<link
rel="component"
href="@threadlabs/looma/components/ui-combobox/ui-combobox.html"
>
<ui-combobox
label="Teams"
name="teams"
multiple
disclosure
placeholder="Add a team…"
>
<option
value="design"
selected
>Design</option>
<option
value="docs"
selected
>Docs</option>
<option
value="platform"
>Platform</option>
</ui-combobox><script setup>
import { UiCombobox } from "@threadlabs/looma";
</script>
<template>
<UiCombobox
label="Teams"
name="teams"
:multiple="true"
:disclosure="true"
placeholder="Add a team…"
>
<option
value="design"
selected
>Design</option>
<option
value="docs"
selected
>Docs</option>
<option
value="platform"
>Platform</option>
</UiCombobox>
</template>import { UiCombobox } from "@threadlabs/looma";
export default function Example() {
return <UiCombobox
label="Teams"
name="teams"
multiple
disclosure
placeholder="Add a team…"
>
<option
value="design"
selected
>Design</option>
<option
value="docs"
selected
>Docs</option>
<option
value="platform"
>Platform</option>
</UiCombobox>;
}/** @jsxImportSource preact */
import { UiCombobox } from "@threadlabs/looma";
export default function Example() {
return <UiCombobox
label="Teams"
name="teams"
multiple
disclosure
placeholder="Add a team…"
>
<option
value="design"
selected
>Design</option>
<option
value="docs"
selected
>Docs</option>
<option
value="platform"
>Platform</option>
</UiCombobox>;
}<script>
import { UiCombobox } from "@threadlabs/looma";
</script>
<UiCombobox
label="Teams"
name="teams"
multiple
disclosure
placeholder="Add a team…"
>
<option
value="design"
selected
>Design</option>
<option
value="docs"
selected
>Docs</option>
<option
value="platform"
>Platform</option>
</UiCombobox>import { Component } from "@angular/core";
import { UiCombobox } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiCombobox],
template: `
<div
ui-combobox
[label]="'Teams'"
[name]="'teams'"
[multiple]="true"
[disclosure]="true"
[placeholder]="'Add a team…'"
>
<option
value="design"
selected
>Design</option>
<option
value="docs"
selected
>Docs</option>
<option
value="platform"
>Platform</option>
</div>
`,
})
export default class Example {}help and label-visibility="sr-only"
Help adds a hint button with a tooltip; label-visibility keeps the label for assistive technology while hiding it visually.
Code
<link
rel="component"
href="@threadlabs/looma/components/ui-combobox/ui-combobox.html"
>
<ui-combobox
label="Destination"
label-visibility="sr-only"
help="Pick where the shipment should be delivered."
disclosure
placeholder="Destination"
>
<option
value="north"
>North terminal</option>
<option
value="west"
>West terminal</option>
</ui-combobox><script setup>
import { UiCombobox } from "@threadlabs/looma";
</script>
<template>
<UiCombobox
label="Destination"
label-visibility="sr-only"
help="Pick where the shipment should be delivered."
:disclosure="true"
placeholder="Destination"
>
<option
value="north"
>North terminal</option>
<option
value="west"
>West terminal</option>
</UiCombobox>
</template>import { UiCombobox } from "@threadlabs/looma";
export default function Example() {
return <UiCombobox
label="Destination"
labelVisibility="sr-only"
help="Pick where the shipment should be delivered."
disclosure
placeholder="Destination"
>
<option
value="north"
>North terminal</option>
<option
value="west"
>West terminal</option>
</UiCombobox>;
}/** @jsxImportSource preact */
import { UiCombobox } from "@threadlabs/looma";
export default function Example() {
return <UiCombobox
label="Destination"
labelVisibility="sr-only"
help="Pick where the shipment should be delivered."
disclosure
placeholder="Destination"
>
<option
value="north"
>North terminal</option>
<option
value="west"
>West terminal</option>
</UiCombobox>;
}<script>
import { UiCombobox } from "@threadlabs/looma";
</script>
<UiCombobox
label="Destination"
labelVisibility="sr-only"
help="Pick where the shipment should be delivered."
disclosure
placeholder="Destination"
>
<option
value="north"
>North terminal</option>
<option
value="west"
>West terminal</option>
</UiCombobox>import { Component } from "@angular/core";
import { UiCombobox } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiCombobox],
template: `
<div
ui-combobox
[label]="'Destination'"
[labelVisibility]="'sr-only'"
[help]="'Pick where the shipment should be delivered.'"
[disclosure]="true"
[placeholder]="'Destination'"
>
<option
value="north"
>North terminal</option>
<option
value="west"
>West terminal</option>
</div>
`,
})
export default class Example {}readonly, disabled, and required
Readonly shows a value that cannot change, disabled makes the field unavailable, and required participates in form validation. A named combobox submits the chosen option's value, such as destination=north, not its label.
Code
<link
rel="component"
href="@threadlabs/looma/components/ui-stack/ui-stack.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-combobox/ui-combobox.html"
>
<ui-stack
gap="s"
>
<ui-combobox
label="Readonly"
readonly
value="north"
disclosure
>
<option
value="north"
>North terminal</option>
</ui-combobox>
<ui-combobox
label="Disabled"
disabled
disclosure
>
<option
value="north"
>North terminal</option>
</ui-combobox>
<ui-combobox
label="Required"
name="destination"
required
disclosure
>
<option
value="north"
>North terminal</option>
<option
value="west"
>West terminal</option>
</ui-combobox>
</ui-stack><script setup>
import { UiCombobox, UiStack } from "@threadlabs/looma";
</script>
<template>
<UiStack
gap="s"
>
<UiCombobox
label="Readonly"
:readonly="true"
value="north"
:disclosure="true"
>
<option
value="north"
>North terminal</option>
</UiCombobox>
<UiCombobox
label="Disabled"
:disabled="true"
:disclosure="true"
>
<option
value="north"
>North terminal</option>
</UiCombobox>
<UiCombobox
label="Required"
name="destination"
:required="true"
:disclosure="true"
>
<option
value="north"
>North terminal</option>
<option
value="west"
>West terminal</option>
</UiCombobox>
</UiStack>
</template>import { UiCombobox, UiStack } from "@threadlabs/looma";
export default function Example() {
return <UiStack
gap="s"
>
<UiCombobox
label="Readonly"
readOnly
value="north"
disclosure
>
<option
value="north"
>North terminal</option>
</UiCombobox>
<UiCombobox
label="Disabled"
disabled
disclosure
>
<option
value="north"
>North terminal</option>
</UiCombobox>
<UiCombobox
label="Required"
name="destination"
required
disclosure
>
<option
value="north"
>North terminal</option>
<option
value="west"
>West terminal</option>
</UiCombobox>
</UiStack>;
}/** @jsxImportSource preact */
import { UiCombobox, UiStack } from "@threadlabs/looma";
export default function Example() {
return <UiStack
gap="s"
>
<UiCombobox
label="Readonly"
readOnly
value="north"
disclosure
>
<option
value="north"
>North terminal</option>
</UiCombobox>
<UiCombobox
label="Disabled"
disabled
disclosure
>
<option
value="north"
>North terminal</option>
</UiCombobox>
<UiCombobox
label="Required"
name="destination"
required
disclosure
>
<option
value="north"
>North terminal</option>
<option
value="west"
>West terminal</option>
</UiCombobox>
</UiStack>;
}<script>
import { UiCombobox, UiStack } from "@threadlabs/looma";
</script>
<UiStack
gap="s"
>
<UiCombobox
label="Readonly"
readonly
value="north"
disclosure
>
<option
value="north"
>North terminal</option>
</UiCombobox>
<UiCombobox
label="Disabled"
disabled
disclosure
>
<option
value="north"
>North terminal</option>
</UiCombobox>
<UiCombobox
label="Required"
name="destination"
required
disclosure
>
<option
value="north"
>North terminal</option>
<option
value="west"
>West terminal</option>
</UiCombobox>
</UiStack>import { Component } from "@angular/core";
import { UiCombobox, UiStack } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiStack, UiCombobox],
template: `
<div
ui-stack
[gap]="'s'"
>
<div
ui-combobox
[label]="'Readonly'"
[readonly]="true"
[value]="'north'"
[disclosure]="true"
>
<option
value="north"
>North terminal</option>
</div>
<div
ui-combobox
[label]="'Disabled'"
[disabled]="true"
[disclosure]="true"
>
<option
value="north"
>North terminal</option>
</div>
<div
ui-combobox
[label]="'Required'"
[name]="'destination'"
[required]="true"
[disclosure]="true"
>
<option
value="north"
>North terminal</option>
<option
value="west"
>West terminal</option>
</div>
</div>
`,
})
export default class Example {}selected values
selectedValues sets the chosen options from data, here a required field with a fixed option list. The consumer owns the selection: each change the user makes is reported as selected-values-change with the new list of values, which Vue binds two-way with v-model:selected-values. A form submits one regions entry per value.
Code
<link
rel="component"
href="@threadlabs/looma/components/ui-combobox/ui-combobox.html"
>
<ui-combobox
label="Regions"
name="regions"
multiple
auto-highlight="single"
select-on-tab
required
disclosure
selected-values="["north","west"]"
>
<option
value="north"
>North</option>
<option
value="south"
>South</option>
<option
value="east"
>East</option>
<option
value="west"
>West</option>
</ui-combobox><script setup lang="ts">
import { ref } from "vue";
import { UiCombobox as Combobox } from "@threadlabs/looma";
const regions = [
{ value: "north", label: "North" },
{ value: "south", label: "South" },
{ value: "east", label: "East" },
{ value: "west", label: "West" }
];
const selected = ref<readonly string[]>(["north", "west"]);
</script>
<template>
<Combobox v-model:selected-values="selected" label="Regions" name="regions" multiple auto-highlight="single" select-on-tab required disclosure>
<option v-for="region in regions" :key="region.value" :value="region.value">{{ region.label }}</option>
</Combobox>
</template>
import { UiCombobox } from "@threadlabs/looma";
export default function Example() {
return <UiCombobox
label="Regions"
name="regions"
multiple
autoHighlight="single"
selectOnTab
required
disclosure
selectedValues={["north","west"]}
>
<option
value="north"
>North</option>
<option
value="south"
>South</option>
<option
value="east"
>East</option>
<option
value="west"
>West</option>
</UiCombobox>;
}/** @jsxImportSource preact */
import { UiCombobox } from "@threadlabs/looma";
export default function Example() {
return <UiCombobox
label="Regions"
name="regions"
multiple
autoHighlight="single"
selectOnTab
required
disclosure
selectedValues={["north","west"]}
>
<option
value="north"
>North</option>
<option
value="south"
>South</option>
<option
value="east"
>East</option>
<option
value="west"
>West</option>
</UiCombobox>;
}<script>
import { UiCombobox } from "@threadlabs/looma";
</script>
<UiCombobox
label="Regions"
name="regions"
multiple
autoHighlight="single"
selectOnTab
required
disclosure
selectedValues={["north","west"]}
>
<option
value="north"
>North</option>
<option
value="south"
>South</option>
<option
value="east"
>East</option>
<option
value="west"
>West</option>
</UiCombobox>import { Component } from "@angular/core";
import { UiCombobox } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiCombobox],
template: `
<div
ui-combobox
[label]="'Regions'"
[name]="'regions'"
[multiple]="true"
[autoHighlight]="'single'"
[selectOnTab]="true"
[required]="true"
[disclosure]="true"
[selectedValues]="['north', 'west']"
>
<option
value="north"
>North</option>
<option
value="south"
>South</option>
<option
value="east"
>East</option>
<option
value="west"
>West</option>
</div>
`,
})
export default class Example {}Browse-first suggestions
Open the list on focus and leave authored results visible while the consumer controls which options are supplied.
Code
<link
rel="component"
href="@threadlabs/looma/components/ui-combobox/ui-combobox.html"
>
<ui-combobox
label="Fruit"
open-on-focus
filter="none"
>
<option
value="apple"
>Apple</option>
<option
value="pear"
>Pear</option>
<span
slot="empty"
>No fruit found</span>
</ui-combobox><script setup>
import { UiCombobox } from "@threadlabs/looma";
</script>
<template>
<UiCombobox
label="Fruit"
:open-on-focus="true"
filter="none"
>
<option
value="apple"
>Apple</option>
<option
value="pear"
>Pear</option>
<template #empty>
<span>No fruit found</span>
</template>
</UiCombobox>
</template>import { UiCombobox } from "@threadlabs/looma";
export default function Example() {
return <UiCombobox
label="Fruit"
openOnFocus
filter="none"
slots={{
empty: <span>No fruit found</span>,
}}
>
<option
value="apple"
>Apple</option>
<option
value="pear"
>Pear</option>
</UiCombobox>;
}/** @jsxImportSource preact */
import { UiCombobox } from "@threadlabs/looma";
export default function Example() {
return <UiCombobox
label="Fruit"
openOnFocus
filter="none"
slots={{
empty: <span>No fruit found</span>,
}}
>
<option
value="apple"
>Apple</option>
<option
value="pear"
>Pear</option>
</UiCombobox>;
}<script>
import { UiCombobox } from "@threadlabs/looma";
</script>
{#snippet empty()}
<span>No fruit found</span>
{/snippet}
<UiCombobox
label="Fruit"
openOnFocus
filter="none"
slots={{ empty }}
>
<option
value="apple"
>Apple</option>
<option
value="pear"
>Pear</option>
</UiCombobox>import { Component } from "@angular/core";
import { UiCombobox } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiCombobox],
template: `
<div
ui-combobox
[label]="'Fruit'"
[openOnFocus]="true"
[filter]="'none'"
>
<option
value="apple"
>Apple</option>
<option
value="pear"
>Pear</option>
<ng-container ngProjectAs="[slot='empty']">
<span>No fruit found</span>
</ng-container>
</div>
`,
})
export default class Example {}Descriptions and tags
A row can carry context under its label (data-description) and a short tag after it (data-tag, with data-tag-tone for a status tone or data-tag-color for a category colour). The label stays the option's name, what filtering matches, and what the field shows once chosen; the tag and description are read as its description.
Code
<link
rel="component"
href="@threadlabs/looma/components/ui-combobox/ui-combobox.html"
>
<ui-combobox
label="Directory"
placeholder="Search people, businesses, and places…"
disclosure
>
<optgroup
label="People"
>
<option
value="p-riley"
data-description="Harbor Supply Co."
data-tag="Contact"
data-tag-color="rebeccapurple"
>Riley Kim</option>
<option
value="p-sam"
data-description="Northwind Traders"
data-tag="Inactive"
data-tag-tone="warning"
>Sam Ortiz</option>
</optgroup>
<optgroup
label="Businesses"
>
<option
value="b-harbor"
data-description="Portland, OR"
data-tag="Business"
data-tag-color="teal"
>Harbor Supply Co.</option>
<option
value="b-northwind"
data-description="Tacoma, WA"
data-tag="Business"
data-tag-color="teal"
>Northwind Traders</option>
</optgroup>
<optgroup
label="Places"
>
<option
value="l-dock"
data-description="Portland, OR"
>Dock 4 warehouse</option>
<option
value="l-yard"
data-description="Tacoma, WA"
data-tag="Closed"
data-tag-tone="danger"
>East yard</option>
</optgroup>
</ui-combobox><script setup>
import { UiCombobox } from "@threadlabs/looma";
</script>
<template>
<UiCombobox
label="Directory"
placeholder="Search people, businesses, and places…"
:disclosure="true"
>
<optgroup
label="People"
>
<option
value="p-riley"
data-description="Harbor Supply Co."
data-tag="Contact"
data-tag-color="rebeccapurple"
>Riley Kim</option>
<option
value="p-sam"
data-description="Northwind Traders"
data-tag="Inactive"
data-tag-tone="warning"
>Sam Ortiz</option>
</optgroup>
<optgroup
label="Businesses"
>
<option
value="b-harbor"
data-description="Portland, OR"
data-tag="Business"
data-tag-color="teal"
>Harbor Supply Co.</option>
<option
value="b-northwind"
data-description="Tacoma, WA"
data-tag="Business"
data-tag-color="teal"
>Northwind Traders</option>
</optgroup>
<optgroup
label="Places"
>
<option
value="l-dock"
data-description="Portland, OR"
>Dock 4 warehouse</option>
<option
value="l-yard"
data-description="Tacoma, WA"
data-tag="Closed"
data-tag-tone="danger"
>East yard</option>
</optgroup>
</UiCombobox>
</template>import { UiCombobox } from "@threadlabs/looma";
export default function Example() {
return <UiCombobox
label="Directory"
placeholder="Search people, businesses, and places…"
disclosure
>
<optgroup
label="People"
>
<option
value="p-riley"
data-description="Harbor Supply Co."
data-tag="Contact"
data-tag-color="rebeccapurple"
>Riley Kim</option>
<option
value="p-sam"
data-description="Northwind Traders"
data-tag="Inactive"
data-tag-tone="warning"
>Sam Ortiz</option>
</optgroup>
<optgroup
label="Businesses"
>
<option
value="b-harbor"
data-description="Portland, OR"
data-tag="Business"
data-tag-color="teal"
>Harbor Supply Co.</option>
<option
value="b-northwind"
data-description="Tacoma, WA"
data-tag="Business"
data-tag-color="teal"
>Northwind Traders</option>
</optgroup>
<optgroup
label="Places"
>
<option
value="l-dock"
data-description="Portland, OR"
>Dock 4 warehouse</option>
<option
value="l-yard"
data-description="Tacoma, WA"
data-tag="Closed"
data-tag-tone="danger"
>East yard</option>
</optgroup>
</UiCombobox>;
}/** @jsxImportSource preact */
import { UiCombobox } from "@threadlabs/looma";
export default function Example() {
return <UiCombobox
label="Directory"
placeholder="Search people, businesses, and places…"
disclosure
>
<optgroup
label="People"
>
<option
value="p-riley"
data-description="Harbor Supply Co."
data-tag="Contact"
data-tag-color="rebeccapurple"
>Riley Kim</option>
<option
value="p-sam"
data-description="Northwind Traders"
data-tag="Inactive"
data-tag-tone="warning"
>Sam Ortiz</option>
</optgroup>
<optgroup
label="Businesses"
>
<option
value="b-harbor"
data-description="Portland, OR"
data-tag="Business"
data-tag-color="teal"
>Harbor Supply Co.</option>
<option
value="b-northwind"
data-description="Tacoma, WA"
data-tag="Business"
data-tag-color="teal"
>Northwind Traders</option>
</optgroup>
<optgroup
label="Places"
>
<option
value="l-dock"
data-description="Portland, OR"
>Dock 4 warehouse</option>
<option
value="l-yard"
data-description="Tacoma, WA"
data-tag="Closed"
data-tag-tone="danger"
>East yard</option>
</optgroup>
</UiCombobox>;
}<script>
import { UiCombobox } from "@threadlabs/looma";
</script>
<UiCombobox
label="Directory"
placeholder="Search people, businesses, and places…"
disclosure
>
<optgroup
label="People"
>
<option
value="p-riley"
data-description="Harbor Supply Co."
data-tag="Contact"
data-tag-color="rebeccapurple"
>Riley Kim</option>
<option
value="p-sam"
data-description="Northwind Traders"
data-tag="Inactive"
data-tag-tone="warning"
>Sam Ortiz</option>
</optgroup>
<optgroup
label="Businesses"
>
<option
value="b-harbor"
data-description="Portland, OR"
data-tag="Business"
data-tag-color="teal"
>Harbor Supply Co.</option>
<option
value="b-northwind"
data-description="Tacoma, WA"
data-tag="Business"
data-tag-color="teal"
>Northwind Traders</option>
</optgroup>
<optgroup
label="Places"
>
<option
value="l-dock"
data-description="Portland, OR"
>Dock 4 warehouse</option>
<option
value="l-yard"
data-description="Tacoma, WA"
data-tag="Closed"
data-tag-tone="danger"
>East yard</option>
</optgroup>
</UiCombobox>import { Component } from "@angular/core";
import { UiCombobox } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiCombobox],
template: `
<div
ui-combobox
[label]="'Directory'"
[placeholder]="'Search people, businesses, and places…'"
[disclosure]="true"
>
<optgroup
label="People"
>
<option
value="p-riley"
data-description="Harbor Supply Co."
data-tag="Contact"
data-tag-color="rebeccapurple"
>Riley Kim</option>
<option
value="p-sam"
data-description="Northwind Traders"
data-tag="Inactive"
data-tag-tone="warning"
>Sam Ortiz</option>
</optgroup>
<optgroup
label="Businesses"
>
<option
value="b-harbor"
data-description="Portland, OR"
data-tag="Business"
data-tag-color="teal"
>Harbor Supply Co.</option>
<option
value="b-northwind"
data-description="Tacoma, WA"
data-tag="Business"
data-tag-color="teal"
>Northwind Traders</option>
</optgroup>
<optgroup
label="Places"
>
<option
value="l-dock"
data-description="Portland, OR"
>Dock 4 warehouse</option>
<option
value="l-yard"
data-description="Tacoma, WA"
data-tag="Closed"
data-tag-tone="danger"
>East yard</option>
</optgroup>
</div>
`,
})
export default class Example {}Discard unfinished search
Browse a small authored list on focus; leaving the multiple combobox clears unfinished text and keeps chosen items.
Code
<link
rel="component"
href="@threadlabs/looma/components/ui-combobox/ui-combobox.html"
>
<ui-combobox
label="Teams"
multiple
clear-query-on-blur
open-on-focus
disclosure
>
<option
value="design"
>Design</option>
<option
value="docs"
>Docs</option>
<option
value="platform"
>Platform</option>
</ui-combobox><script setup>
import { UiCombobox } from "@threadlabs/looma";
</script>
<template>
<UiCombobox
label="Teams"
:multiple="true"
:clear-query-on-blur="true"
:open-on-focus="true"
:disclosure="true"
>
<option
value="design"
>Design</option>
<option
value="docs"
>Docs</option>
<option
value="platform"
>Platform</option>
</UiCombobox>
</template>import { UiCombobox } from "@threadlabs/looma";
export default function Example() {
return <UiCombobox
label="Teams"
multiple
clearQueryOnBlur
openOnFocus
disclosure
>
<option
value="design"
>Design</option>
<option
value="docs"
>Docs</option>
<option
value="platform"
>Platform</option>
</UiCombobox>;
}/** @jsxImportSource preact */
import { UiCombobox } from "@threadlabs/looma";
export default function Example() {
return <UiCombobox
label="Teams"
multiple
clearQueryOnBlur
openOnFocus
disclosure
>
<option
value="design"
>Design</option>
<option
value="docs"
>Docs</option>
<option
value="platform"
>Platform</option>
</UiCombobox>;
}<script>
import { UiCombobox } from "@threadlabs/looma";
</script>
<UiCombobox
label="Teams"
multiple
clearQueryOnBlur
openOnFocus
disclosure
>
<option
value="design"
>Design</option>
<option
value="docs"
>Docs</option>
<option
value="platform"
>Platform</option>
</UiCombobox>import { Component } from "@angular/core";
import { UiCombobox } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiCombobox],
template: `
<div
ui-combobox
[label]="'Teams'"
[multiple]="true"
[clearQueryOnBlur]="true"
[openOnFocus]="true"
[disclosure]="true"
>
<option
value="design"
>Design</option>
<option
value="docs"
>Docs</option>
<option
value="platform"
>Platform</option>
</div>
`,
})
export default class Example {}Wrap selected items before scrolling
grow with maxRows lets a multiple field grow through three rows before additional selections scroll vertically inside the field. The input remains reachable by keyboard.
Code
<link
rel="component"
href="@threadlabs/looma/components/ui-container/ui-container.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-combobox/ui-combobox.html"
>
<ui-container
fill
>
<ui-combobox
label="Topics"
multiple
grow
max-rows="3"
disclosure
placeholder="Add a topic…"
>
<option
value="research"
selected
>Research and discovery</option>
<option
value="design"
selected
>Interface design</option>
<option
value="content"
selected
>Content strategy</option>
<option
value="accessibility"
selected
>Accessibility review</option>
<option
value="delivery"
>Delivery</option>
</ui-combobox>
</ui-container><script setup>
import { UiCombobox, UiContainer } from "@threadlabs/looma";
</script>
<template>
<UiContainer
:fill="true"
>
<UiCombobox
label="Topics"
:multiple="true"
:grow="true"
:max-rows="3"
:disclosure="true"
placeholder="Add a topic…"
>
<option
value="research"
selected
>Research and discovery</option>
<option
value="design"
selected
>Interface design</option>
<option
value="content"
selected
>Content strategy</option>
<option
value="accessibility"
selected
>Accessibility review</option>
<option
value="delivery"
>Delivery</option>
</UiCombobox>
</UiContainer>
</template>import { UiCombobox, UiContainer } from "@threadlabs/looma";
export default function Example() {
return <UiContainer
fill
>
<UiCombobox
label="Topics"
multiple
grow
maxRows={3}
disclosure
placeholder="Add a topic…"
>
<option
value="research"
selected
>Research and discovery</option>
<option
value="design"
selected
>Interface design</option>
<option
value="content"
selected
>Content strategy</option>
<option
value="accessibility"
selected
>Accessibility review</option>
<option
value="delivery"
>Delivery</option>
</UiCombobox>
</UiContainer>;
}/** @jsxImportSource preact */
import { UiCombobox, UiContainer } from "@threadlabs/looma";
export default function Example() {
return <UiContainer
fill
>
<UiCombobox
label="Topics"
multiple
grow
maxRows={3}
disclosure
placeholder="Add a topic…"
>
<option
value="research"
selected
>Research and discovery</option>
<option
value="design"
selected
>Interface design</option>
<option
value="content"
selected
>Content strategy</option>
<option
value="accessibility"
selected
>Accessibility review</option>
<option
value="delivery"
>Delivery</option>
</UiCombobox>
</UiContainer>;
}<script>
import { UiCombobox, UiContainer } from "@threadlabs/looma";
</script>
<UiContainer
fill
>
<UiCombobox
label="Topics"
multiple
grow
maxRows={3}
disclosure
placeholder="Add a topic…"
>
<option
value="research"
selected
>Research and discovery</option>
<option
value="design"
selected
>Interface design</option>
<option
value="content"
selected
>Content strategy</option>
<option
value="accessibility"
selected
>Accessibility review</option>
<option
value="delivery"
>Delivery</option>
</UiCombobox>
</UiContainer>import { Component } from "@angular/core";
import { UiCombobox, UiContainer } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiContainer, UiCombobox],
template: `
<div
ui-container
[fill]="true"
>
<div
ui-combobox
[label]="'Topics'"
[multiple]="true"
[grow]="true"
[maxRows]="3"
[disclosure]="true"
[placeholder]="'Add a topic…'"
>
<option
value="research"
selected
>Research and discovery</option>
<option
value="design"
selected
>Interface design</option>
<option
value="content"
selected
>Content strategy</option>
<option
value="accessibility"
selected
>Accessibility review</option>
<option
value="delivery"
>Delivery</option>
</div>
</div>
`,
})
export default class Example {}API
Package: @threadlabs/looma · Native root: div
Attributes
| Name | Description | Property | Type | Default | Allowed values |
|---|---|---|---|---|---|
allow-create | Offers the current query as a new option, reported as create-item. When nothing else matches, the offer is highlighted, so Enter creates it; with nothing highlighted, Enter in multiple mode commits the typed text as a token separator does. | allowCreate | boolean | false | — |
allow-free-text | Allows values that do not match an authored option. | allowFreeText | boolean | false | — |
auto-highlight | single highlights the only enabled authored suggestion while typing. Ambiguous results and an empty query stay unhighlighted. none preserves manual highlighting with the arrow keys. | autoHighlight | keyword | none | none, single |
clear-query-on-blur | In multiple mode, clears unfinished search text when focus leaves the whole combobox, preserving selected items. A controlled query reports query-change with an empty query; the consumer must reflect it back. Does not choose or create an option. The default preserves unfinished text. Single mode keeps its existing selection and free-text resolution. | clearQueryOnBlur | boolean | false | — |
clearable | Adds a clear button to the field that empties the input and clears the selection, reported as value-change with kind "clear". It is disabled when there is nothing to clear. In multiple mode it clears only the typed text; chosen items stay. | clearable | boolean | false | — |
disabled | Disables the input and every button in the field: the chosen-item badges, the clear and disclosure buttons, and the help button. The list closes and cannot be opened, the value cannot be cleared, and a named combobox submits nothing with its form. | disabled | boolean | false | — |
disclosure | Adds a chevron button that opens and closes the full, unfiltered list of options, for users who would rather browse than type. ArrowDown opens the same list with or without it. | disclosure | boolean | false | — |
filter | How the list narrows as the user types. label lists the options whose label contains the typed text, ignoring case. none lists every authored option, for options already narrowed elsewhere, such as a server search, so a result that matched on data other than its label still shows. Leaving the field still commits an option whose label matches the text, or the only one listed. | filter | keyword | label | label, none |
grow | Let a multiple-selection field wrap and grow with its selected items and typing space. Without grow it stays one horizontally scrolling row. | grow | boolean | false | — |
help | Help text for the field. When set, a help button appears beside the field and opens a tooltip with this text; while it is open, the input is described by it. | help | string | | — |
invalid | Exposes an externally validated error on the input and field. It remains until the consumer clears it, independently of native validation. | invalid | boolean | false | — |
items | The selected items in multiple mode, as JSON; a named combobox submits each one's value. A form reset returns to these items, or to authored selected options when unset. | items | list(object)
| — | — |
label | The field's label, tied to its input and shown above it; required adds an asterisk. It also names the suggestion list. Pair it with labelVisibility="sr-only" when the context already labels the field visibly. | label | string | | — |
label-visibility | visible shows the label above the field; sr-only hides it visually but keeps it as the input's accessible name. Use sr-only only when the surrounding layout already makes the field's purpose clear. | labelVisibility | keyword | visible | visible, sr-only |
loading | Marks the suggestion list busy while the consumer loads options. It announces loading and suppresses the empty state and create offer until cleared. | loading | boolean | false | — |
max-rows | Optional maximum visible rows when grow is enabled. Extra selections scroll vertically inside the field. Omit it for unbounded growth; single-selection fields are unchanged. | maxRows | integer | — | — |
multiple | Lets the user choose several options. Each choice becomes a removable badge before the input, the list stays open between picks, and choosing a checked option removes it. items-change then reports the whole list of items, and selected-values-change the list of their values; use selectedValues or items, not value, in this mode. | multiple | boolean | false | — |
muted | Shows the selected field value with supporting-text emphasis, for an inherited or default choice. Suggestions keep normal emphasis and the field stays interactive. | muted | boolean | false | — |
name | The form field name the combobox submits its value under, never the label shown in the field. Single mode sends one entry: the selected option's value, the typed text when allowFreeText is set and no option is selected, or an empty string. Multiple mode sends one entry per chosen item's value, and nothing when none is chosen. A disabled combobox sends nothing. | name | string | | — |
open-on-focus | Opens suggestions when the input receives focus or is clicked. A committed selection shows all choices until typing filters them; an explicitly supplied query keeps its filtering. | openOnFocus | boolean | false | — |
placeholder | Hint text shown in the empty input, as on a native input. It disappears as the user types, so it never replaces label. | placeholder | string | | — |
query | The input's text, when the consumer controls it. Once set, the consumer owns the text: typing reports query-change, and the input returns to query unless query is updated to match. Leave it unset to let the combobox manage its own text, showing the chosen option's label. | query | string | — | — |
readonly | Shows the current value without letting it change: the text can be selected but not edited, the list does not open, and the clear, disclosure, and badge remove buttons are disabled. A named combobox still submits its value. | readonly | boolean | false | — |
required | Requires a value: adds an asterisk to the label, marks the input required for native form validation, and makes validation report "A value is required." when the field is left empty. In multiple mode a chosen item satisfies it, whatever is left in the input. | required | boolean | false | — |
select-on-tab | Commits a highlighted authored suggestion on forward Tab, then lets focus advance normally. In multiple mode a selected suggestion is retained without duplication or removal. Shift+Tab does not commit, and Tab never chooses the create offer. | selectOnTab | boolean | false | — |
selected-values | The selected option values in multiple mode, when the consumer controls the selection. Without it or items, authored options with selected set the initial selection. Once set, the consumer owns it: each value shows as a badge with its authored option's label, adding or removing one reports selected-values-change with the new list, and the badges follow selectedValues, not the user, unless it is updated to match. Setting it reports nothing. It takes precedence over items; leave it unset to let the combobox keep its own selection. | selectedValues | list(string) | — | — |
size | Field height, padding, and type size, matching Button's sizes so a field and a button of one size share a row. sm is the small control height (2rem by default) with smaller text, for compact rows such as a table row or a filter bar; md is the standard control height; lg is taller with roomier padding. Touch input keeps body-size field text without inflating the visible size ladder. The label keeps its size. | size | keyword | md | sm, md, lg |
token-separators | Keys that commit the typed text in multiple mode, as KeyboardEvent key values such as "," or ";". The text is added as the option whose label matches it, ignoring case, or with allowCreate is reported as create-item; otherwise it stays in the input. | tokenSeparators | list(string) | — | — |
value | The selected value in single mode, which a named combobox submits with its form. The field shows the matching option's label, and a form reset returns to this value. In a strict combobox, typing a search does not change the selection; leaving with no match restores its label. If omitted, the first authored option with selected sets the initial value. | value | string | — | — |
Framework props
| Name | Type | Default | Allowed values |
|---|---|---|---|
allowCreate | boolean | false | — |
allowFreeText | boolean | false | — |
autoHighlight | "none" | "single" | none | none, single |
clearable | boolean | false | — |
clearQueryOnBlur | boolean | false | — |
disabled | boolean | false | — |
disclosure | boolean | false | — |
filter | "label" | "none" | label | label, none |
grow | boolean | false | — |
help | string | | — |
invalid | boolean | false | — |
items | readonly ({ readonly id: string; readonly value: string; readonly label: string; readonly group?: string | null; readonly disabled?: boolean | null; readonly description?: string | null; readonly tag?: { readonly label: string; readonly tone?: "neutral" | "accent" | "info" | "success" | "warning" | "danger" | null; readonly color?: string | null } | null })[]
| — | — |
label | string | | — |
labelVisibility | "visible" | "sr-only" | visible | visible, sr-only |
loading | boolean | false | — |
maxRows | number | — | — |
multiple | boolean | false | — |
muted | boolean | false | — |
name | string | | — |
openOnFocus | boolean | false | — |
placeholder | string | | — |
query | string | — | — |
readonly | boolean | false | — |
required | boolean | false | — |
selectedValues | readonly (string)[] | — | — |
selectOnTab | boolean | false | — |
size | "sm" | "md" | "lg" | md | sm, md, lg |
tokenSeparators | readonly (string)[] | — | — |
value | string | — | — |
Events
| Name | Detail | Description |
|---|---|---|
add-item | object
| Emitted with the declared detail payload. |
create-entry | object
| Emitted with the declared detail payload. |
create-item | object
| Emitted with the declared detail payload. |
free-entry | object
| Emitted with the declared detail payload. |
items-change | list(object)
| Emitted with the declared detail payload. |
options-change | list(object)
| Emitted with the declared detail payload. |
query-change | object
| Emitted with the declared detail payload. |
remove-item | object
| Emitted with the declared detail payload. |
selected-values-change | object
| Emitted with the declared detail payload. |
validation-change | object
| Emitted with the declared detail payload. |
value-change | object
| Emitted with the declared detail payload. |
Slots
| Name | Purpose |
|---|---|
item | Named content for the item region. |
option | Named content for the option region. |
start | Named content for the start region. |
empty | Named content for the empty region. |
footer | Named content for the footer region. |
default | Default child content. |
Design tokens
Set shared theme properties on a theme boundary. Component hooks apply only to their owning component.
Component tokens
| Token | Declared values | Fallback |
|---|---|---|
--ui-combobox-affordance-text | — | var(--ui-text-secondary) |
--ui-combobox-invalid-border | — | var(--ui-field-danger, var(--ui-danger-solid, var(--ui-accent-active))) |
--ui-combobox-item-input-min-inline-size | — | 5rem |
--ui-combobox-item-item-max-inline-size | — | 12rem |
--ui-combobox-item-min-block-size | var(--ui-control-size-md) | var(--ui-control-size-sm) | var(--ui-control-size-lg) | — |
--ui-combobox-label-font-size | — | var(--ui-field-label-font-size, var(--ui-font-size-md)) |
--ui-combobox-label-text | — | var(--ui-text-primary) |
--ui-combobox-max-block-size | — | 18rem |
--ui-combobox-message-text | — | var(--ui-text-secondary) |
--ui-combobox-option-check | — | — |
--ui-combobox-option-text | — | inherit |
--ui-combobox-popup-border | — | var(--ui-overlay-border, var(--ui-border-strong)) |
--ui-combobox-popup-radius | — | var(--ui-overlay-radius, var(--ui-radius-md)) |
--ui-combobox-popup-shadow | — | var(--ui-overlay-shadow, var(--ui-elevation-overlay, var(--ui-shadow-lg))) |
--ui-combobox-popup-surface | — | var(--ui-material-content, var(--ui-surface-elevated)) |
--ui-combobox-radius | — | var(--ui-field-radius, var(--ui-radius-md)) |
Shared tokens
| Token | Fallback |
|---|---|
--ui-accent-active | — |
--ui-accent-solid | — |
--ui-border-default | — |
--ui-border-strong | — |
--ui-border-width | — |
--ui-control-border | var(--ui-border-default) |
--ui-control-focus | var(--ui-accent-solid) |
--ui-control-focus-halo-shadow | — |
--ui-control-inset | — |
--ui-control-inset-block-lg | var(--ui-space-3) |
--ui-control-inset-block-md | var(--ui-space-2) |
--ui-control-inset-block-sm | var(--ui-space-1) |
--ui-control-size-lg | — |
--ui-control-size-md | — |
--ui-control-size-sm | — |
--ui-control-surface | var(--ui-surface-default) |
--ui-control-surface-disabled | var(--ui-surface-muted) |
--ui-danger-solid | var(--ui-accent-active) |
--ui-disabled-text | — |
--ui-elevation-overlay | var(--ui-shadow-lg) |
--ui-field-danger | var(--ui-danger-solid, var(--ui-accent-active)) |
--ui-field-label-font-size | var(--ui-font-size-md) |
--ui-field-radius | var(--ui-radius-md) |
--ui-focus-halo | — |
--ui-focus-ring | — |
--ui-focus-width | — |
--ui-font-size-md | — |
--ui-font-size-sm | — |
--ui-icon-size-sm | — |
--ui-line-height-md | — |
--ui-line-height-tight | — |
--ui-material-content | var(--ui-surface-elevated) |
--ui-motion-ease | — |
--ui-motion-fast | — |
--ui-overlay-border | var(--ui-border-strong) |
--ui-overlay-radius | var(--ui-radius-md) |
--ui-overlay-shadow | var(--ui-elevation-overlay, var(--ui-shadow-lg)) |
--ui-radius-md | — |
--ui-radius-round | — |
--ui-radius-sm | — |
--ui-shadow-lg | — |
--ui-space-0-5 | — |
--ui-space-1 | — |
--ui-space-2 | — |
--ui-space-3 | — |
--ui-space-4 | — |
--ui-surface-default | — |
--ui-surface-elevated | — |
--ui-surface-muted | — |
--ui-text-muted | — |
--ui-text-primary | — |
--ui-text-secondary | — |