@klungland/multiselect
v0.1.1
Published
A dependency-light, floating-ui-positioned multiselect component for Svelte 5.
Maintainers
Readme
Multiselect
Published as @klungland/multiselect. A dependency-light multiselect for Svelte 5, positioned with @floating-ui/dom. It flips to the best side when space is tight, and shrinks to fit rather than overflowing the viewport. Icons are Lucide.
Features
- Boundary aware, flip-then-shrink positioning via
@floating-ui/dom. Prefers switching sides over overflowing, and shrinks the dropdown to fit if there's still no room. - Search-as-you-type filtering over the option list.
- Keyboard navigation: arrow keys to highlight, Enter to select, Escape to close.
- Select all / deselect all (shown by default when
maxSelectis unbounded). - Free-text option creation via
allowUserOptions, with per-context reset viacontextKey. - Configurable dropdown scrolling:
scrollX/scrollY(both defaulttrue) cap the dropdown to a fixed size with scrollbars. Set either tofalseto let it size itself to its content instead, staying viewport-bounded. - Works as a single-select (
maxSelect={1}) or multi-select. - No required styling. The component ships as an unstyled skeleton, with an optional green theme you can opt into.
- Real dependencies:
@floating-ui/domand@lucide/svelte.
Installation
npm install @klungland/multiselectUsage
<script lang="ts">
import { Multiselect } from '@klungland/multiselect';
import '@klungland/multiselect/theme-green.css'; // optional
let selected: (string | { label: string; value: string | number })[] = [];
const options = [
{ label: 'Apple', value: 'apple' },
{ label: 'Banana', value: 'banana' },
{ label: 'Cherry', value: 'cherry' },
];
</script>
<Multiselect {options} bind:selected maxSelect={null} placeholder="Pick some fruit..." allowUserOptions />Props
| Prop | Type | Default | Description |
| ------------------- | ---------------------------------- | ------- | ----------------------------------------------------------------------------------------- |
| options | MultiselectOption[] | — | The selectable options |
| selected | MultiselectOption[] (bindable) | [] | Currently selected options |
| maxSelect | number \| null | — | Max selections; 1 behaves as single-select, null is unbounded |
| minSelect | number | 0 | Minimum selections required, blocks removal below this count |
| allowUserOptions | boolean | false | Lets users type and add options not in the provided list |
| contextKey | unknown | — | Changing this resets any user-added options (e.g. switching filter context) |
| selectAllOption | boolean | — | Force show/hide the "Select all" row; defaults to shown when maxSelect is unbounded |
| maxOptions | number \| null | null | Cap how many options are rendered in the dropdown |
| width | string | — | Trigger width, as any CSS length ('20rem', '300px', '50%'). Defaults to filling its parent (100%) |
| scrollX | boolean | true | Dropdown scrolls horizontally at a fixed (trigger) width. false: sizes to content instead (never narrower than the trigger, free to grow wider), staying viewport-bounded and falling back to scroll only if content doesn't fit |
| scrollY | boolean | true | Dropdown scrolls vertically, capped at height. false: grows to fit all options, staying viewport-bounded and falling back to scroll only if content doesn't fit |
| height | number | 330 | Pixel cap on dropdown height while scrollY is true. Ignored while scrollY is false |
| disabled | boolean | false | Disables all interaction |
| loading | boolean | false | Shows a loading message instead of the option list |
| placeholder | string | '' | Placeholder text when nothing is selected |
| required | boolean | false | Adds a hidden validator input for native form validation |
| id | string | — | Element id; also used to derive the dropdown's id |
| onChange | () => void | — | Fires on any selection change |
| onOpen | () => void | — | Fires when the dropdown opens |
| onClose | () => void | — | Fires when the dropdown closes, only if the selection actually changed |
| onRemove | () => void | — | Fires when a tag is removed while the dropdown is closed |
| onRemoveAll | () => void | — | Fires when all tags are cleared while the dropdown is closed |
MultiselectOption is string | number | { label: string; value: string | number }.
Styling
The component ships unstyled by default: just its shape (pill trigger, rounded dropdown), no colors. Colors come entirely from CSS custom properties, so you can either bring your own or opt into the built-in green theme.
Option A: the built-in green theme
<script>
import '@klungland/multiselect/theme-green.css';
</script>This sets every color variable below to a green palette (accent oklch(0.72 0.19 149.6)) with good contrast against a dark background. Importing it applies to every Multiselect on the page. Override individual variables afterwards (globally, or scoped to one instance via an inline style on a wrapper) to adjust it.
Option B: bring your own colors
Set these custom properties yourself (on :root, a wrapper element, or per-instance):
.multiselect {
--multiselect-bg: #1c1e2b;
--multiselect-text: #e4e6f1;
--multiselect-muted: #8a8da3;
--multiselect-border-color: #34374d;
--multiselect-hover-bg: #292c40;
--multiselect-accent: #8b7cf6;
--multiselect-accent-strong: #6c5ce7;
--multiselect-accent-contrast: #1c1e2b;
--multiselect-tag-bg: rgba(139, 124, 246, 0.16);
--multiselect-tag-text: #cabdfb;
}| Variable | Used for |
| --------------------------------- | ------------------------------------------------------------------ |
| --multiselect-bg | Trigger and dropdown background |
| --multiselect-text | Primary text |
| --multiselect-muted | Placeholder text, chevron, status messages |
| --multiselect-border-color | Borders and dividers |
| --multiselect-hover-bg | Hover background on options and icon buttons |
| --multiselect-accent | Checked checkboxes, hover text color on options |
| --multiselect-accent-strong | Reserved for a stronger accent state (e.g. focus rings) in your own overrides |
| --multiselect-accent-contrast | Checkmark color drawn on top of --multiselect-accent |
| --multiselect-tag-bg | Selected tag chip background |
| --multiselect-tag-text | Selected tag chip text |
Shape variables (--multiselect-radius-full, --multiselect-radius-md, --multiselect-radius-sm, --multiselect-z-index, --multiselect-disabled-opacity) have sensible defaults and are independent of theming. Override them the same way if needed.
Development
This repo is the library itself, built with SvelteKit's library mode.
npm install
npm run dev # demo/playground at src/routes/+page.svelte
npm run check # svelte-check
npm run build # builds the demo app + packages the library into dist/Project structure
src/
lib/
Multiselect.svelte the component
index.ts public exports
types.ts MultiselectOption / MultiselectProps
theme-green.css optional green color theme
internal/
floating.ts floating-ui positioning (scrollX/scrollY logic)
utils.ts pure option helpers (getLabel, getValue, sorting)
IconButton.svelte
Tag.svelte
Checkbox.svelte
OptionRow.svelte
SelectAllRow.svelte
StatusMessage.svelte
routes/
+page.svelte demo/dev playgroundCredits
Positioning is powered by Floating UI (MIT). Icons are from Lucide (ISC). Both ship as regular dependencies, installed alongside this package rather than bundled into it.
License
MIT. See LICENSE.
