Skip to content

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).

Terminal window
npm install @loomidev/select lit
import "@loomidev/select";

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" },
];

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 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>
<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 immediately
<loomi-select disabled placeholder="What is your nationality" data="..."></loomi-select>
<loomi-select readonly placeholder="What is your nationality" data="..."></loomi-select>

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>
<loomi-select searchable label-key="country" value-key="code" data="..."></loomi-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>

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.

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>

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>
const el = document.querySelector("loomi-select");
el.addEventListener("select", (e) => {
console.log(e.detail); // { value, label, values }
});

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>

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>

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>

For the library-wide baseline, see Foundations — Accessibility.

For the shared container and viewport rules, see Foundations — Responsive behavior.

For theme activation, token overrides, and contrast guidance, see Foundations — Dark mode.

AttributeDefaultDescription
name(blank)Submitted with the form.
placeholderSelect OneTrigger text when nothing is selected.
label(blank)Floating label (takes precedence over placeholder).
label-positiondefaultdefault 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-keylabelRow key used as each option’s display text.
value-keyvalueRow 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.
searchablefalseShow a search box. (boolean)
multiplefalseAllow multiple selection. (boolean)
max-selectable-1Max items when multiple (-1 = no limit).
disabledfalseDisable the select. (boolean)
readonlyfalseRead-only (cannot open). (boolean)
requiredfalseMarks the field required. (boolean)
sizemediumsmall | regular | medium | big
variantdefaultdefault | minimal (bottom border only, no box)
empty-placeholderNo options availableText 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-clearingfalseRemove the default bottom margin. (boolean)

Parts: trigger, panel. Methods: reset(), validate().

SlotDescription
(default)Content placed inside the component.
EventDescription
changeFired when the value is committed or changed.
loomi-empty-actionFired when the empty-state action is activated.
loomi-selectFired when an option is selected.
<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>

<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.

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.

Terminal window
cd /path/to/your-app
npm install @loomidev/select lit

If 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:

Terminal window
cd /path/to/your-copy-of-loomiui/components
pnpm --filter @loomidev/select build
pnpm --filter @loomidev/select typecheck

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.

Terminal window
cd /path/to/your-laravel-app
npm install @loomidev/select lit
npm run dev
resources/js/app.js
import "@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.

app.component.ts
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>

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.

  • @loomidev/core
  • @loomidev/theme