Examples
Single select
<select id="country">
<option value="vn">Vietnam</option>
<option value="jp">Japan</option>
<option value="us">United States</option>
</select>
import ForgeSelect from "forge-select";
new ForgeSelect("#country", {
placeholder: "Select a country",
searchable: true,
});
Multiple select
new ForgeSelect("#tags", {
multiple: true,
clearable: true,
placeholder: "Select one or more",
});
Reorderable tags (drag & drop)
new ForgeSelect("#skills", {
multiple: true,
sortable: true,
placeholder: "Select skills, then drag to reorder",
});
Drag a tag with the mouse, touch, or a pen to reorder it; with a tag focused, Alt+Left/Alt+Right does the same via keyboard. getValue() (and the change event) always reflect the current tag order — on a native <select multiple>, the underlying <option> elements are reordered to match too, so a plain <form> submission serializes values in the dragged order.
Async / AJAX data source
new ForgeSelect("#users", {
ajax: {
url: (query) => `/api/users?q=${encodeURIComponent(query)}`,
debounce: 300,
transform: (response) =>
response.items.map((u) => ({ value: u.id, label: u.name })),
},
});
Async pagination (load more on scroll)
new ForgeSelect("#users", {
ajax: {
url: "/api/users",
pagination: true,
params: (query, page) => ({ q: query, page }),
transform: (response) => ({
options: response.items.map((u) => ({ value: u.id, label: u.name })),
hasMore: response.hasMore,
}),
},
});
Scrolling near the bottom of the dropdown fetches the next page and appends it to the list instead of replacing it; typing a new search query still resets back to page 0 as usual.
Tags mode (allow creating new options)
new ForgeSelect("#skills", {
multiple: true,
allowCreate: true,
placeholder: "Add a skill",
});
Custom templates
new ForgeSelect("#country", {
templateResult: (option) => `<span class="flag flag-${option.value}"></span> ${option.label}`,
templateSelection: (option) => option.label,
});
Option groups
new ForgeSelect("#country", {
data: [
{
label: "Asia",
options: [
{ value: "vn", label: "Vietnam" },
{ value: "jp", label: "Japan" },
],
},
{
label: "Americas",
options: [{ value: "us", label: "United States" }],
},
],
});
Tree select (nested options)
new ForgeSelect("#categories", {
multiple: true,
data: [
{
value: "electronics", label: "Electronics",
children: [
{ value: "phones", label: "Phones" },
{ value: "laptops", label: "Laptops" },
],
},
{
value: "clothing", label: "Clothing",
children: [
{ value: "shirts", label: "Shirts" },
{ value: "shoes", label: "Shoes" },
],
},
],
});
Nodes with children start collapsed; click the twisty (▶/▼) to expand. In multiple mode, selecting "Electronics" also selects "Phones" and "Laptops"; selecting only some of a parent's descendants shows it as indeterminate.
Rich items (avatar + name + description)
Options with avatar/description get a built-in rich layout — no template needed, and the fields are XSS-safe:
new ForgeSelect("#users", {
itemHeight: 52, // taller rows for the two-line layout
data: [
{
value: "1",
label: "Ana Trần",
description: "ana@example.com",
avatar: "https://example.com/avatars/ana.png",
},
// …works great with 1,000+ items: virtualization kicks in automatically
],
});
The same data can be rendered with a fully custom template instead. Prefer returning a DOM Node built with textContent — it stays XSS-safe even when the data comes from users or a remote API (a string return value is injected as raw HTML, so only use it with trusted or pre-sanitized content):
new ForgeSelect("#users", {
templateResult: (o) => {
const row = document.createElement("span");
const img = document.createElement("img");
img.className = "my-avatar";
if (/^(https:|data:image\/)/.test(o.avatar ?? "")) img.src = o.avatar;
const name = document.createElement("strong");
name.textContent = o.label;
const email = document.createElement("small");
email.textContent = o.description ?? "";
row.append(img, name, email);
return row;
},
templateSelection: (o) => {
const span = document.createElement("span");
span.textContent = o.label;
return span;
},
});
Virtual scrolling (large lists)
Lists over ~100 rows are virtualized automatically — only the visible window is in the DOM, and row content is cached so templates run once per option. Set virtualScroll: false to opt out, or itemHeight if your rows are taller than the default 36px:
new ForgeSelect("#big-list", {
data: largeArrayOfThousandsOfOptions, // nothing else to configure
});
React
The official forge-select-react package wraps Forge Select in a controlled component — no manual mounting required:
npm install forge-select forge-select-react
import { useState } from "react";
import { ForgeSelectReact } from "forge-select-react";
import "forge-select/styles.css";
function CountrySelect() {
const [value, setValue] = useState(null);
return (
<ForgeSelectReact
searchable
data={[
{ value: "vn", label: "Vietnam" },
{ value: "jp", label: "Japan" },
]}
value={value}
onChange={setValue}
/>
);
}
data, templateResult, plugins, and other constructor-only options are read once at mount; remount with a different key to change them. See the package README for the full prop reference.
If you'd rather not add the extra dependency, mount the core library directly:
import { useEffect, useRef } from "react";
import ForgeSelect from "forge-select";
function CountrySelect() {
const ref = useRef(null);
useEffect(() => {
const select = new ForgeSelect(ref.current, { searchable: true });
select.on("change", (value) => console.log(value));
return () => select.destroy();
}, []);
return (
<select ref={ref}>
<option value="vn">Vietnam</option>
<option value="jp">Japan</option>
</select>
);
}
Vue
The official forge-select-vue package wraps Forge Select as a Vue 3 component with v-model support:
npm install forge-select forge-select-vue
<script setup>
import { ref } from "vue";
import { ForgeSelectVue } from "forge-select-vue";
import "forge-select/styles.css";
const value = ref(null);
const options = {
searchable: true,
data: [
{ value: "vn", label: "Vietnam" },
{ value: "jp", label: "Japan" },
],
};
</script>
<template>
<ForgeSelectVue :options="options" v-model="value" />
</template>
options is read once when the instance is created; force a remount with a different :key to apply new data/templates/plugins. See the package README for the full reference.
If you'd rather not add the extra dependency, mount the core library directly:
<template>
<select ref="el">
<option value="vn">Vietnam</option>
<option value="jp">Japan</option>
</select>
</template>
<script setup>
import { onMounted, onBeforeUnmount, ref } from "vue";
import ForgeSelect from "forge-select";
const el = ref(null);
let select;
onMounted(() => {
select = new ForgeSelect(el.value, { searchable: true });
});
onBeforeUnmount(() => select?.destroy());
</script>
Svelte
<script>
import { onMount, onDestroy } from "svelte";
import ForgeSelect from "forge-select";
let el;
let select;
onMount(() => {
select = new ForgeSelect(el, { searchable: true });
});
onDestroy(() => select?.destroy());
</script>
<select bind:this={el}>
<option value="vn">Vietnam</option>
<option value="jp">Japan</option>
</select>