# Slim Select keeps your native select in the DOM and layers configurable dropdown behaviour on top of it

> Slim Select is a dependency free TypeScript replacement for the native select element, with search, optgroups, remote data, modal mode on mobile and theming through CSS custom properties. The native element stays in the page, which is what makes the data API optional and the accessibility story plausible.

**brianvoe/slim-select** — Slim advanced select dropdown

- Repository: https://github.com/brianvoe/slim-select
- Website: http://slimselectjs.com
- Stars: 1,333 · Forks: 226
- Language: TypeScript
- License: MIT
- Published: 2026-10-17 · Updated: 2026-10-17 · Language: en
- Canonical page: https://hysenlabs.com/projects/brianvoe-slim-select

## The native select stays, which is the whole integration contract

The quick start is two lines of JavaScript and an existing element. You import the module and its styles, then construct it against a selector:

```bash
npm install slim-select
```

```javascript
import SlimSelect from 'slim-select'
import 'slim-select/styles' // or: import 'slim-select/scss'

new SlimSelect({
  select: '#selectElement'
})
```

The element it points at is an ordinary select with ordinary options. That is the design decision with the widest consequences, and it cuts both ways.

What it buys you is that the data can come from somewhere else without changing the contract. You can pass an array of options and optgroups instead of, or in addition to, the native option elements, and a component that needs a hundred entries can be given them as data rather than as a hundred lines of markup. The README also notes that the component inherits styles, classes and the title tooltip from the native select, so a select that already carries your form's classes keeps them.

What it costs is that a native select is a constrained starting point. It renders below or above the viewport depending on the browser, it has no room for a search box, and its options are text. Everything Slim Select adds is a layer that has to reposition, trap focus and re-render that markup, and the settings list below is largely a list of the decisions that layer has to make.

The option data model is explicit about its own states: text is required, value defaults to text, html is used for display when set, and there are separate flags for selected, display, disabled, mandatory, placeholder, plus class, style and a plain data object for data attributes. The display flag is the interesting one, described as false if a native option is hidden or display:none, which is how the component knows a hidden option is a real option rather than a discarded one.

## The settings object is mostly positioning, and mobile defaults to a modal

The README publishes every setting with its default, which is a documentation choice worth appreciating, since the list covers many keys and the positioning ones are the ones you will actually change.

Three of them are the positioning core. contentLocation defaults to document.body, which is where the dropdown markup is attached, and that default is why the component works inside a container with overflow hidden. contentPosition can be absolute, relative or fixed, and contentWidth accepts a value, a minimum such as >500px, or a maximum such as <500px. openPosition can be auto, up or down, and auto is the one that flips the panel when there is not enough room below.

The fourth is modal, and its default is the surprising part. modal can be off, on or mobile, and the default is mobile, described as a centered panel with a backdrop that is mobile friendly by default. So on a narrow screen the dropdown becomes a full panel with a backdrop and an optional modalTitle header, without the application asking for it.

The rest of the list falls into three groups. Text you may need to translate: searchPlaceholder, searchText for no results, searchingText, a resultsText template of {count} results available, deselectText, removeText, placeholderText and the multi-select strings. Limits: minSelected at 0, maxSelected at 1000, timeoutDelay at 200, maxValuesShown at 20 with a maxValuesMessage template, and the select-all checkbox in multi-select with its own selectAllText and unselectAllText. And behaviour: showSearch defaulting to true, focusSearch, keepSearch, searchHighlight, closeOnSelect, allowDeselect, hideSelected, multiString, keepOrder, showOptionTooltips, and alwaysOpen.

The ariaLabel default of Combobox is in that list too, which tells you the accessibility work is a first-class part of the configuration rather than an afterthought.

## Search has two mechanisms, and picking the wrong one is silent

Search appears in the feature list three ways: search with highlighting, remote or API search, and options the user can add. The events section then shows that the first two are separate settings, and that they are not the same thing.

There is a search event, which handles remote or API search. Its signature takes the current input text as searchValue, the currently selected options, and a catalog that is the baseline list restored when the search clears. It returns a promise or a plain array of partial options and optgroups. So a remote search is a function you write, and the component renders whatever comes back.

There is a searchFilter event, described as the local filter used when events.search is not set, taking an option and the search string and returning a boolean. If you wire up the wrong one, the symptom is a search box that appears to do nothing against a server, and the README's phrasing is precise enough that the intent is clear once you have read it.

The addable event is the third. It takes a value, and returns the new option, a promise for one, or an Error, so a rejected add is a value rather than an exception to catch. The user-facing half of that is the addableText template, Press "Enter" to add {value}, which is the pattern the rest of the library uses: a template string with named placeholders rather than concatenation.

Two other events shape when things happen. beforeChange receives the new and old selections and can return false to veto, and afterChange receives the new selection. There is beforeOpen and afterOpen, and an event whose return value can cancel a close, with the reason passed in so the code can distinguish a select, a deselect, a click outside, a toggle, escape, tab, the modal or an API call. The reason list is the useful part: it is what lets one handler implement a confirmation on close without guessing which path triggered it.

## Theming is a list of custom properties you have to fill in

The styling approach is a set of CSS custom properties, and the README frames it as avoiding a fight with class specificity, which is the correct complaint about most component libraries.

You set them on a wrapper around your select, or on the root element for a global theme:

```css
.my-form {
  --ss-primary-color: #2563eb;
  --ss-bg-color: #ffffff;
  --ss-font-color: #1e293b;
  --ss-border-color: #e2e8f0;
  --ss-border-radius: 8px;
  --ss-main-height: 44px;
}
```

Two of those are worth a comment. The main height at 44 pixels is a touch target size, so the default is likely smaller and this is how you make a list of options comfortable on a phone. The border radius is the only shape control in that list, which tells you the component's visual design is deliberately flat and that everything else about its appearance comes from your existing form.

The distribution side of styling is a choice you make at install time. The package publishes a styles export pointing at the compiled CSS and an scss export pointing at the source SCSS file, and the import line in the quick start is the same either way, with a comment showing the alternative. If your build has a Sass pipeline, importing the SCSS source is what lets you compile the variables into your own stylesheet rather than overriding them afterwards.

The size is stated plainly in the feature list: about 66KB of JavaScript, about 16KB gzipped, and about 12KB of CSS, about 2KB gzipped. For a component that replaces an element the browser already ships, that is the number to argue with, and the honest argument is that you are buying search, keyboard handling and theming rather than size.

## Accessibility claims come with a test count

The feature list claims WCAG 2.1 Level AA accessibility with ARIA, keyboard and screen reader support, and two details in the same list are the ones to check rather than the claim itself.

The first is keyboard type-ahead when search is turned off. The showSearch setting is documented as false meaning you type letters or digits to jump to a matching option, so a user who cannot or does not want to use the search box still has a way to move through a long list from the keyboard. That is a real requirement for a listbox, and it is rarely offered.

The second is that prefers-reduced-motion is respected, which is a CSS media query the component honours rather than something you configure.

The optgroup support is more than labels. Groups can have select-all, which is a checkbox next to the search box in multi-select mode, and they can be closable, with the closable field taking off, open or close, so a long grouped list can be collapsed.

What supports all of this is the test suite. The feature list states 477 unit tests and 46 Playwright end to end tests, and the README badge says 523 passing. The repository backs that with a vitest config and a playwright.config.ts, an e2e/ directory and separate unit test workflow, and a release.js script at the root. For a component whose selling point is behaviour a user feels rather than an API a developer calls, an end to end suite is the part that actually validates the claim.

## Five entry points, and the wrapper packages are separate builds

The package manifest describes more than one distribution, and the exports map is the clearest statement of the layout.

The root entry has types, an import condition pointing at an ES module build and a require condition pointing at a CommonJS build. There is a separate vue entry, with its own types, an ES module build and a UMD build, and a separate react entry with the same three conditions. Then there are the styles entry pointing at the compiled CSS and the scss entry pointing at the SCSS source.

The legacy fields matter for older toolchains: main, browser and unpkg all point at dist/slimselect.js, and module points at dist/slimselect.es.js. The project is declared as an ES module with a Node engine of 20.19.0 or later, which is a real constraint if your build tooling is older than that.

The README describes the wrappers differently, and the difference matters for choosing one. The Vue component works with v-model, and the React component comes with hooks and ref access. Ref access in particular implies the underlying instance is reachable, which is what you need for imperative methods, and the README links separate Methods and Events documentation pages for the full list.

The React and Vue builds are not thin re-exports in the manifest, since each has its own output directory under dist. That means three separate bundles rather than one, and it is the reason a project that only needs the vanilla build never downloads the wrappers.

## Version skew between the branch and the last release

The licence is MIT with a LICENSE file at the root, and the project is authored by Brian Voelker with the homepage slimselectjs.com, where the full documentation, live demos and copy-paste examples live rather than in the README.

The versioning is worth a close look. The recent releases listed for the repository are v2.9.0 on 2024-09-11, v3.0.0 on 2025-10-21 and v4.0.0 on 2026-06-26. The package.json on the default branch reads version 4.6.1. So the branch version is ahead of the newest listed release, which means a version number in a repository file may not be the version you get from the registry.

Read CHANGELOG.md, which is in the repository root, before bumping a major.

Maintenance facts otherwise look ordinary: the repository is not archived, the last push was on 2026-10-13, and the topic list is vanilla-javascript, select, search and multiselect, which is a component with a clear scope and no ambitions beyond it.

## Conclusion

Adopt Slim Select when you need a searchable, multi-select capable dropdown in a stack with no framework, or when you need a Vue or React wrapper without pulling in a component library. Do not adopt it expecting a design system: it ships plain CSS and SCSS, and the theming surface is a set of custom properties you have to fill in yourself. Verify first the version you install, because package.json on the default branch reads 4.6.1 while the most recent published tag in the release list is v4.0.0, and check the modal default of mobile before shipping on small screens.

## FAQ

### How do I install Slim Select?

With `npm install slim-select`, or from a CDN with a script tag pointing at https://unpkg.com/slim-select@latest/dist/slimselect.js and a stylesheet link to https://unpkg.com/slim-select@latest/dist/slimselect.css. The package requires Node 20.19.0 or later.

### What is the default modal behaviour in Slim Select?

The modal setting defaults to mobile, with options off, on and mobile. The mobile default gives a centered panel with a backdrop, described as mobile friendly by default, and a modalTitle setting supplies the header above the option list in modal view.

### How do I theme Slim Select?

With CSS custom properties prefixed --ss-, set on a wrapper around your select or on the root element for a global theme. The README names primary colour, background colour, font colour, border colour, border radius and main height as the example set, and the full token list is on the style documentation page.

### Does Slim Select work with remote data sources?

Yes, through the search event, which receives the current input text, the currently selected options and a baseline catalog, and returns a promise or an array of options. A local searchFilter event is used instead when the search event is not set.

## Sources

- [brianvoe/slim-select on GitHub](https://github.com/brianvoe/slim-select)
- [License: MIT](https://github.com/brianvoe/slim-select/blob/master/LICENSE)
- [Project website](http://slimselectjs.com)
- [README](https://github.com/brianvoe/slim-select/blob/master/README.md)
- [Releases](https://github.com/brianvoe/slim-select/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/brianvoe-slim-select
