Select
Select replaces a native <select> element with a fully styleable custom dropdown. It supports single selection, multi-tag selection, live search, option icons, default values, and a programmatic API for open/close/dispose.
The native <select> remains in the DOM as the anchor. FrontAlign builds the visible UI immediately after it and keeps the hidden input and native element value synchronized on every selection.
Getting Started
Place a <select> with a unique id in your HTML. Pass the id (or the element itself) as the first argument to new Select().
import { Select } from 'frontalign';
const select = new Select('framework-select', {
inputName: 'framework',
placeholder: 'Choose a framework',
data: [
{ value: 'react', name: 'React' },
{ value: 'vue', name: 'Vue' },
{ value: 'svelte', name: 'Svelte' },
{ value: 'solid', name: 'Solid' }
]
});
How It Works
When new Select() is called, the component:
- Creates a
.custom-selectroot div and inserts it after the anchor<select>in the DOM - Builds a
.selected-optiondisplay area, an.options-containerdropdown, and a hidden<input>for form submission - Binds a document-level click listener to close the dropdown when clicking outside
- Keeps the original
<select>.valueand the hidden input in sync on every selection — and dispatches a nativechangeevent on the original element
The anchor element is hidden with select.custom-select { display: none } in the CSS, so it is never visible but always participates in form serialization via the hidden input.
Generated Markup
<!-- Your markup -->
<select id="framework-select"></select>
| Element | Class / Role | Purpose |
|---|---|---|
| Anchor | select#id | Native element used as the initialization target and change event source. |
| Root | .custom-select | Generated focusable wrapper with role="combobox" and aria-expanded. |
| Display | .selected-option | Shows placeholder, selected label+icon, or multi-select tags. |
| Dropdown | .options-container | Animated panel containing the optional search input and option rows. |
| Option | .select-option | Clickable row generated from each object in the data array. |
| Hidden input | input[type="hidden"] | Carries the selected value(s) under inputName for form submission. |
Multiple Select
Set multiple: true to allow more than one selected item. Selected values render as removable tags inside the display area. The synced value becomes a comma-separated string ("blue,green,purple").
When the display area is too narrow to show all tags, extra ones collapse into a +N more badge — calculated via ResizeObserver on every layout change.
import { Select } from 'frontalign';
new Select('colors-select', {
inputName: 'colors',
multiple: true,
placeholder: 'Choose colors',
data: [
{ value: 'blue', name: 'Blue' },
{ value: 'green', name: 'Green' },
{ value: 'orange', name: 'Orange' },
{ value: 'purple', name: 'Purple' },
{ value: 'red', name: 'Red' }
]
});
Selecting a duplicate value in multiple mode is silently ignored.
Search
Set search: true to inject a text input at the top of the dropdown. As the user types, options are filtered by their lowercased name value in real time. Non-matching options are hidden with display: none — they are not removed from the DOM.
import { Select } from 'frontalign';
new Select('language-select', {
inputName: 'language',
search: true,
placeholder: 'Search a language',
data: [
{ value: 'js', name: 'JavaScript' },
{ value: 'ts', name: 'TypeScript' },
{ value: 'py', name: 'Python' },
{ value: 'go', name: 'Go' },
{ value: 'rust', name: 'Rust' },
{ value: 'java', name: 'Java' },
{ value: 'cpp', name: 'C++' }
]
});
searchandmultiplecan be combined freely. The search field filters which options are visible — already-selected tags are not affected.
Default Values
Use defaultValue to preselect one or more options at initialization time. Pass a single value for single mode, or an array of values for multiple mode.
import { Select } from 'frontalign';
// Single default
new Select('plan-select', {
inputName: 'plan',
placeholder: 'Choose a plan',
defaultValue: 'pro',
data: [
{ value: 'free', name: 'Free' },
{ value: 'pro', name: 'Pro' },
{ value: 'enterprise', name: 'Enterprise' }
]
});
// Multiple defaults
new Select('tags-select', {
inputName: 'tags',
multiple: true,
placeholder: 'Choose tags',
defaultValue: ['css', 'javascript'],
data: [
{ value: 'html', name: 'HTML' },
{ value: 'css', name: 'CSS' },
{ value: 'javascript', name: 'JavaScript' },
{ value: 'typescript', name: 'TypeScript' }
]
});
Icon Options
Add an icon URL to any option object to render an image before the label — both in the dropdown list and in the selected display area. Any image URL works: devicons, custom SVGs, or your own CDN assets.
import { Select } from 'frontalign';
new Select('stack-select', {
inputName: 'stack',
placeholder: 'Choose a stack',
data: [
{ value: 'react', name: 'React', icon: 'https://cdn.jsdelivr.net/gh/devicons/devicon/icons/react/react-original.svg' },
{ value: 'vue', name: 'Vue', icon: 'https://cdn.jsdelivr.net/gh/devicons/devicon/icons/vuejs/vuejs-original.svg' },
{ value: 'next', name: 'Next.js', icon: 'https://cdn.jsdelivr.net/gh/devicons/devicon/icons/nextjs/nextjs-original.svg' },
{ value: 'svelte', name: 'Svelte', icon: 'https://cdn.jsdelivr.net/gh/devicons/devicon/icons/svelte/svelte-original.svg' },
{ value: 'node', name: 'Node.js', icon: 'https://cdn.jsdelivr.net/gh/devicons/devicon/icons/nodejs/nodejs-original.svg' }
]
});
Search + Multiple Combined
search and multiple work together out of the box. Useful for large option lists where users need to find and tag several values quickly.
import { Select } from 'frontalign';
new Select('skills-select', {
inputName: 'skills',
multiple: true,
search: true,
placeholder: 'Search and select skills',
defaultValue: ['css', 'js'],
data: [
{ value: 'html', name: 'HTML' },
{ value: 'css', name: 'CSS' },
{ value: 'js', name: 'JavaScript' },
{ value: 'ts', name: 'TypeScript' },
{ value: 'react', name: 'React' },
{ value: 'vue', name: 'Vue' },
{ value: 'node', name: 'Node.js' },
{ value: 'figma', name: 'Figma' }
]
});
Programmatic API
Every Select instance exposes public methods for external control.
const select = new Select('my-select', { data: [...] });
// Open the dropdown
select.open();
// Close the dropdown
select.close();
// Toggle open/close
select.toggle();
// Programmatically select an option (must match a data entry)
select.select({ value: 'vue', name: 'Vue' });
// Remove a selected value by its value string (multiple mode)
select.remove('vue');
// Destroy the instance: removes DOM, unbinds all listeners, disconnects ResizeObserver
select.dispose();
| Method | Description |
|---|---|
open() | Opens the dropdown panel with opacity and translate animation. |
close() | Closes the panel and hides it after the animation duration (200ms default). |
toggle() | Opens if closed, closes if open. |
select(opt) | Selects an option programmatically. Accepts a full option object . |
remove(value) | Removes a value from the selection in multiple mode. No-op in single mode. |
dispose() | Removes generated DOM, unbinds document click listener, disconnects ResizeObserver. |
Listening for Changes
Every select() and remove() call dispatches a native change event on the original anchor element. Use a standard addEventListener to react to it.
const anchorEl = document.getElementById('role-select');
const select = new Select(anchorEl, { data: [...] });
anchorEl.addEventListener('change', function () {
console.log('Current value:', this.value);
// single mode → "developer"
// multiple mode → "css,javascript,typescript"
});
This means Select integrates naturally with any form library or event delegation system that listens on native DOM events.
Data Format
Each entry in the data array must have value and name. The icon field is optional.
| Field | Type | Required | Description |
|---|---|---|---|
value | string | number | Yes | Submitted value and internal identifier. Used by defaultValue and remove(). |
name | string | Yes | Visible label in the option list, selected display, and search filtering (data-name attribute). |
icon | string | No | Image URL rendered before the label in both the option row and selected display. |
const data = [
{ value: 'react', name: 'React', icon: 'https://cdn.jsdelivr.net/gh/devicons/devicon/icons/react/react-original.svg' },
{ value: 'vue', name: 'Vue', icon: 'https://cdn.jsdelivr.net/gh/devicons/devicon/icons/vuejs/vuejs-original.svg' },
{ value: 'svelte', name: 'Svelte', icon: 'https://cdn.jsdelivr.net/gh/devicons/devicon/icons/svelte/svelte-original.svg' },
{ value: 'node', name: 'Node.js' } // icon is optional
];
Configuration Reference
| Option | Type | Default | Description |
|---|---|---|---|
inputName | string | "select" | The name attribute of the generated hidden input used for form submission. |
multiple | boolean | false | Enables multi-tag selection. Selected values are joined as a comma-separated string. |
defaultValue | string | number | Array | null | null | Pre-selected value(s) applied at initialization. Pass an array in multiple mode. |
data | Array | [] | Option objects used to build the dropdown. Each must include value and name. |
search | boolean | false | Injects a live search input at the top of the dropdown. Filters by lowercased name. |
placeholder | string | "--SELECT--" | Text shown in the display area when no value is selected. |
SSR & Cleanup Notes
SSR guard — The constructor checks for window and document before doing anything. Importing Select in a server-rendered environment is safe — the constructor exits early and produces no side effects. When using the CDN build, initialize inside a DOMContentLoaded listener or at the bottom of <body>.
Cleanup — Each instance creates DOM nodes, registers a document-level click listener, and may create a ResizeObserver for multi-tag overflow. Call dispose() when the select is no longer needed to remove all three.
// Vanilla JS — manual cleanup
const select = new Select('#my-select', { data: [...] });
// Later, on teardown:
select.dispose();
When using useSelect in React, cleanup is handled automatically — the hook calls dispose() in its useEffect return function on unmount. No manual teardown is needed.
Nuxt / Vue — call dispose() inside onUnmounted:
import { onMounted, onUnmounted } from 'vue';
let select;
onMounted(() => {
select = new Select('#my-select', { data: [...] });
});
onUnmounted(() => select?.dispose());
Runtime Behavior Reference
| Behavior | Result |
|---|---|
| Single selection | Replaces display content, syncs hidden input and anchor value, dispatches change, closes dropdown. |
| Multiple selection | Appends a tag, updates comma-separated value string, dispatches change. Dropdown stays open. |
| Duplicate selection | Silently ignored in multiple mode — the same value cannot be added twice. |
| Tag removal | Removes tag, updates value string, dispatches change on anchor. |
| Tag overflow | Extra tags are hidden and collapsed into a +N more badge, recalculated on every resize via ResizeObserver. |
| Search filtering | Options not matching the query are hidden with display: none. DOM is not modified. |
| Outside click | Closes the dropdown through a document-level delegated click listener registered at init time. |
| Dropdown animation | Opens with opacity 0→1 and translateY(-8px)→0. Closes in reverse. Duration: 200ms (configurable via Select.variables.animationDuration). |