Select
<loomi-select> — a themeable custom select. Supports a data array (or JSON string),
manual <option> children, search, multiple selection, images and a floating label.
Form-associated: submits the selected value(s) under name (comma-joined for multiple).
npm install @loomidev/select litimport "@loomidev/select";Basic Usage (Data-Driven)
Section titled “Basic Usage (Data-Driven)”Pass an array via the .data property, or a JSON string via the data attribute. Keys
default to label / value.
<loomi-select name="country" label="Country" data='[{"label":"Ghana","value":"gh"},{"label":"Nigeria","value":"ng"},{"label":"Kenya","value":"ke"}]'></loomi-select>document.querySelector("loomi-select").data = [ { label: "Ghana", value: "gh" }, { label: "Nigeria", value: "ng" },];Custom Key Names
Section titled “Custom Key Names”It’s not always practical to rewrite your data to use label/value keys. Remap them
with label-key / value-key.
<loomi-select label-key="country" value-key="code" data='[{"country":"Ghana","code":"gh"},{"country":"Nigeria","code":"ng"}]'></loomi-select>Placeholder vs Label
Section titled “Placeholder vs Label”placeholder shows hint text that disappears once something is selected. label is
always visible (floats above the trigger once a value is chosen). When both are set,
label takes precedence.
<loomi-select placeholder="What is your nationality" data="..."></loomi-select><loomi-select label="Where are you from?" required data="..."></loomi-select>Selecting a Value by Default
Section titled “Selecting a Value by Default”<loomi-select selected-value="gh" placeholder="What is your nationality" data="..."></loomi-select>selected-value isn’t just a one-time initial value — setting it again later (as an
attribute or the .selectedValue property) re-syncs the visible selection, which is
useful for swapping which record a select reflects (e.g. re-pointing one “assignee”
select at a different task) without re-creating the element.
document.querySelector("loomi-select").selectedValue = "ng"; // updates immediatelyDisabled & Readonly
Section titled “Disabled & Readonly”<loomi-select disabled placeholder="What is your nationality" data="..."></loomi-select><loomi-select readonly placeholder="What is your nationality" data="..."></loomi-select>With Images
Section titled “With Images”Set image-key to the key in your data that holds an image URL, to render a small image
beside each option — handy for “assign to” pickers.
<loomi-select placeholder="Assign task to" label-key="name" value-key="id" image-key="picture" data='[{"id":1,"name":"Ada","picture":"/avatars/ada.jpg"}]'></loomi-select>Searchable Select
Section titled “Searchable Select”<loomi-select searchable label-key="country" value-key="code" data="..."></loomi-select>Empty Select
Section titled “Empty Select”When there’s no data yet (e.g. waiting on an API response), the select shows
empty-placeholder. If searchable is also set, the search box automatically hides
since there’s nothing to search.
<loomi-select searchable empty-placeholder="No countries available" data="[]"></loomi-select>Select Multiple Items
Section titled “Select Multiple Items”Set multiple to allow more than one selection. Unlike the single select, a multiple
select stays open after each pick — click outside it to close.
<loomi-select multiple searchable max-selectable="3" label="Select a country" label-key="country" value-key="code" data="..."></loomi-select>Trying to select past max-selectable blocks the extra selection.
Pre-Selecting Multiple Values
Section titled “Pre-Selecting Multiple Values”Use a comma-separated list for selected-value.
<loomi-select multiple selected-value="gh,ng,ke" label-key="country" value-key="code" data="..."></loomi-select>Manual Options
Section titled “Manual Options”When your data isn’t coming from an array, use plain <option> children instead.
<loomi-select name="gender" placeholder="Select gender"> <option value="male">Male</option> <option value="female">Female</option> <option value="other">Prefer not to say</option></loomi-select>Reacting to Selection
Section titled “Reacting to Selection”const el = document.querySelector("loomi-select");el.addEventListener("select", (e) => { console.log(e.detail); // { value, label, values }});Get the Selected Value on Form Submission
Section titled “Get the Selected Value on Form Submission”Every <loomi-select> participates in ElementInternals form association, so its value
submits like a native form control under whatever name you gave it — comma-joined for
multiple selects.
new FormData(form).get("country"); // "gh"new FormData(form).get("tags"); // "pop,jazz" (multiple)<loomi-select size="small" data="..."></loomi-select><loomi-select size="regular" data="..."></loomi-select><loomi-select size="medium" data="..."></loomi-select><loomi-select size="big" data="..."></loomi-select>Empty State CTA
Section titled “Empty State CTA”When there are no options, empty-placeholder shows the empty message. Add
empty-action-label for a small CTA; clicking it emits loomi-empty-action, and
empty-action-url can navigate directly.
<loomi-select label="Project" empty-placeholder="No projects yet" empty-action-label="Create project"></loomi-select>Field appearance
Section titled “Field appearance”Use variant="minimal" for a bottom-border-only field:
<loomi-select variant="minimal" placeholder="Choose a department"></loomi-select>Use label-position="inside" to keep a compact label inside the top of the field,
with the selected value displayed beneath it:
<loomi-select label="Department" label-position="inside"></loomi-select>Accessibility
Section titled “Accessibility”For the library-wide baseline, see Foundations — Accessibility.
Responsive behavior
Section titled “Responsive behavior”For the shared container and viewport rules, see Foundations — Responsive behavior.
Dark mode
Section titled “Dark mode”For theme activation, token overrides, and contrast guidance, see Foundations — Dark mode.
Attributes
Section titled “Attributes”| Attribute | Default | Description |
|---|---|---|
name | (blank) | Submitted with the form. |
placeholder | Select One | Trigger text when nothing is selected. |
label | (blank) | Floating label (takes precedence over placeholder). |
label-position | default | default keeps the floating label; inside keeps a compact label inside the top of the field. |
data | [] | Options array — property (.data) or JSON-string attribute. |
label-key | label | Row key used as each option’s display text. |
value-key | value | Row key used as each option’s submitted value. |
image-key | (blank) | Key holding an image URL to show beside each option. |
selected-value | (blank) | Default value(s); comma-separated for multiple. |
searchable | false | Show a search box. (boolean) |
multiple | false | Allow multiple selection. (boolean) |
max-selectable | -1 | Max items when multiple (-1 = no limit). |
disabled | false | Disable the select. (boolean) |
readonly | false | Read-only (cannot open). (boolean) |
required | false | Marks the field required. (boolean) |
size | medium | small | regular | medium | big |
variant | default | default | minimal (bottom border only, no box) |
empty-placeholder | No options available | Text shown when there are no options. |
empty-action-label | (blank) | CTA label shown in the empty state. |
empty-action-url | (blank) | Optional URL to navigate to when the empty CTA is clicked. |
no-clearing | false | Remove the default bottom margin. (boolean) |
Parts: trigger, panel. Methods: reset(), validate().
| Slot | Description |
|---|---|
| (default) | Content placed inside the component. |
Events
Section titled “Events”| Event | Description |
|---|---|
change | Fired when the value is committed or changed. |
loomi-empty-action | Fired when the empty-state action is activated. |
loomi-select | Fired when an option is selected. |
Full Example
Section titled “Full Example”<loomi-select name="country" label="What is your nationality" data='[{"label":"Ghana","value":"gh"},{"label":"Nigeria","value":"ng"}]' value-key="value" label-key="label" required selected-value="gh" searchable size="big"></loomi-select>Framework integration
Section titled “Framework integration”<loomi-select> is a standard custom element, so the browser can use it in plain HTML, Blade, React, Vue, Angular, Svelte, Astro, and most other frameworks. The important beginner rule is: install the package, import it once before the tag is rendered, then write the Loomi tag in your template.
Where to run commands
Section titled “Where to run commands”Run install commands from the app where you want to use this component. That means the folder that contains that app’s package.json. Do not run these install commands from packages/select unless you are editing LoomiUI itself.
cd /path/to/your-appnpm install @loomidev/select litIf you are contributing to LoomiUI itself, first move to the top-level components folder. That is where the main package.json for all packages lives, and pnpm --filter ... commands should be run from there:
cd /path/to/your-copy-of-loomiui/componentspnpm --filter @loomidev/select buildpnpm --filter @loomidev/select typecheckChoose your framework
Section titled “Choose your framework”Use the CDN version for prototypes, documentation pages, or a quick reproduction. The import map tells the browser where to find Lit, which Loomi components use internally.
<script type="importmap"> { "imports": { "lit": "https://esm.sh/lit@3.3.3", "lit/": "https://esm.sh/lit@3.3.3/" } }</script><script type="module" src="https://esm.sh/@loomidev/select"></script>
<loomi-select name="country" label="Country" data='[{"label":"Ghana","value":"gh"},{"label":"Nigeria","value":"ng"}]'></loomi-select>In Vite, Webpack, Parcel, Rollup, or a framework build pipeline, install the package and import it once in your main app JavaScript file. After that, you can use the Loomi tag anywhere in your app.
import "@loomidev/select";Because this is a form-capable component, give it a name when it should submit with a native <form>. Read its value with new FormData(form).get("the-name") just like you would for a built-in input.
This component accepts data as a JavaScript property. Use an HTML attribute only for simple strings; use a property when you pass arrays, objects, or functions.
const el = document.querySelector("loomi-select");el.data = [{ label: "Ghana", value: "gh" }, { label: "Nigeria", value: "ng" }];Run the install command from your Laravel project root, then import the component in resources/js/app.js. If your project uses Laravel Vite, npm run dev and npm run build should also be run from the Laravel project root.
cd /path/to/your-laravel-appnpm install @loomidev/select litnpm run devimport "@loomidev/select";<loomi-select name="country" label="Country" data='[{"label":"Ghana","value":"gh"},{"label":"Nigeria","value":"ng"}]'></loomi-select>React can render Loomi tags directly. If you are on React 18, or if you need to pass arrays, objects, or functions, use a ref and assign those values after the component mounts.
import { useEffect, useRef } from "react";import "@loomidev/select";
export function LoomiExample() { const el = useRef(null);
useEffect(() => { el.current.data = [{ label: "Ghana", value: "gh" }, { label: "Nigeria", value: "ng" }]; }, []);
return <loomi-select ref={el}></loomi-select>;}If TypeScript does not recognize the Loomi tag in JSX, add it to your app’s JSX type declarations.
Import the package in the component that uses it, or once in your main Vue file. Vue templates can use Loomi tags directly. For arrays, objects, or functions, pass the value as a JavaScript property instead of as plain text.
<script setup>import { onMounted, ref } from "vue";import "@loomidev/select";
const el = ref(null);
onMounted(() => { el.value.data = [{ label: "Ghana", value: "gh" }, { label: "Nigeria", value: "ng" }];});</script>
<template> <loomi-select ref="el"></loomi-select></template>If Vue warns that the tag is an unknown component, configure compilerOptions.isCustomElement for tags that start with loomi- in your Vite or Vue config.
Import the package once and tell Angular to allow custom HTML tags with CUSTOM_ELEMENTS_SCHEMA. For NgModule apps, add the schema to the module instead of the standalone component.
import { AfterViewInit, CUSTOM_ELEMENTS_SCHEMA, Component, ElementRef, ViewChild } from "@angular/core";import "@loomidev/select";
@Component({ selector: "app-root", standalone: true, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <loomi-select #el></loomi-select> `,})export class AppComponent implements AfterViewInit { @ViewChild("el") el!: ElementRef<any>;
ngAfterViewInit() { this.el.nativeElement.data = [{ label: "Ghana", value: "gh" }, { label: "Nigeria", value: "ng" }]; }}Svelte can import the package inside a component script. Astro can import it in the frontmatter of the page or layout where the tag appears.
<script> import { onMount } from "svelte"; import "@loomidev/select";
let el;
onMount(() => { el.data = [{ label: "Ghana", value: "gh" }, { label: "Nigeria", value: "ng" }]; });</script>
<loomi-select bind:this={el}></loomi-select>---import "@loomidev/select";---
<loomi-select name="country" label="Country" data='[{"label":"Ghana","value":"gh"},{"label":"Nigeria","value":"ng"}]'></loomi-select>Server-side rendering notes
Section titled “Server-side rendering notes”Frameworks such as Next.js, Nuxt, SvelteKit, and Astro sometimes render HTML on the server before browser-only code runs. If your framework complains, move the Loomi import to client-side code. In Next.js, that usually means a component with "use client"; in Nuxt, it often means a .client.ts plugin.
Dependencies
Section titled “Dependencies”@loomidev/core@loomidev/theme