npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@agentnova/lazyselect

v1.0.1

Published

Lightweight, dependency-free multi-select dropdown with server-side pagination, search, and selection persistence.

Downloads

30

Readme

LazySelect

npm version license jsDelivr hits

Lightweight, dependency-free multi-select dropdown with server-side pagination, search, and selection persistence.

A lightweight multi-select dropdown for the case where you have thousands of options on the backend and need to load them on demand. Drop in one <script> tag and go.

<script src="https://cdn.jsdelivr.net/npm/@agentnova/[email protected]/lazyselect.min.js"></script>
<div id="categories"></div>
<script>
  new LazySelect('#categories', { url: '/api/categories' });
</script>
  • Zero dependencies — no jQuery, no React, no Vue
  • Server-side pagination via infinite scroll
  • Debounced search
  • Selections persist across search and pagination (the part homemade versions usually get wrong)
  • Bring-your-own backend via responseAdapter
  • Hidden input sync for plain HTML form submits — no JS config required
  • One-line CDN install, styles included
  • ✅ Bootstrap-friendly defaults, fully overridable CSS

Install

Via CDN (recommended)

<script src="https://cdn.jsdelivr.net/npm/@agentnova/[email protected]/lazyselect.min.js"></script>

Or unpkg:

<script src="https://unpkg.com/@agentnova/lazyselect@1/lazyselect.min.js"></script>

That's it. Styles inject themselves automatically. No CSS file to include.

Self-hosted

<script src="/static/lazyselect.min.js"></script>

npm

npm install @agentnova/lazyselect
import LazySelect from "@agentnova/lazyselect";

Quick start

<div id="my-filter"></div>

<script src="https://cdn.jsdelivr.net/npm/@agentnova/[email protected]/lazyselect.min.js"></script>
<script>
  const ms = new LazySelect('#my-filter', {
    url: '/api/categories',
    placeholder: 'Search categories...',
    onChange: (ids, items) => console.log(ids, items)
  });
</script>

Your endpoint should accept ?page=1&pageSize=10&search=foo and return:

{ "items": [{ "id": 1, "name": "Foo" }, { "id": 2, "name": "Bar" }] }

That's the whole contract. Send back items, get back a working multi-select.


Backend example

# Django
def load_filter_data(request):
    page = int(request.GET.get('page', 1))
    page_size = int(request.GET.get('pageSize', 10))
    search = request.GET.get('search', '')

    qs = MyModel.objects.filter(name__icontains=search)
    start = (page - 1) * page_size
    items = qs[start:start + page_size].values('id', 'name')
    return JsonResponse({'items': list(items)})
// Express / Node
app.get('/api/categories', async (req, res) => {
  const { page = 1, pageSize = 10, search = '' } = req.query;
  const offset = (page - 1) * pageSize;
  const items = await db.query(
    'SELECT id, name FROM categories WHERE name LIKE ? LIMIT ? OFFSET ?',
    [`%${search}%`, +pageSize, offset]
  );
  res.json({ items });
});

If your endpoint returns a different shape, use responseAdapter:

new LazySelect('#x', {
  url: '/api/users',
  responseAdapter: (raw) => ({
    items: raw.results.map(u => ({ id: u.uuid, name: u.full_name }))
  })
});

Options

| Option | Type | Default | Description | |---|---|---|---| | url | string | required | Endpoint that returns paginated items. | | containerKey | string | '' | Sent to the server as container_data. Useful when one endpoint serves multiple dropdowns. | | pageSize | number | 10 | Items per request. | | searchDebounce | number | 300 | Milliseconds to wait after typing before searching. | | placeholder | string | 'Search...' | Input placeholder. | | selectedTextFn | fn(n) | 'N items selected' | Text shown when items are selected. | | noDataText | string | 'No data available' | Empty-state message. | | loadingText | string | 'Loading...' | Loading-state message. | | errorText | string | 'Failed to load' | Error-state message. | | method | string | 'GET' | HTTP method. Use 'POST' to send body as JSON. | | extraParams | object | {} | Static params merged into every request. | | getExtraParams | fn() | null | Returns dynamic params (e.g. dependent dropdown values). | | headers | object | {} | Custom request headers (CSRF, auth, etc). | | responseAdapter | fn(raw) | passthrough | Transform server response into {items, total}. | | resolveNames | fn(ids) | null | Returns Promise<[{id,name}]> to label pre-selected ids. | | onChange | fn(values, items) | null | Fires on every selection change. | | onReady | fn(instance) | null | Fires after first load. | | onError | fn(error) | null | Fires on network/parse errors. | | initialValues | array | [] | Pre-select. Pass [1,2,3] or [{id:1,name:'Foo'}]. | | closeOnSelect | bool | false | Close dropdown after each pick. | | maxHeight | string | '240px' | Dropdown panel height. | | autoInjectStyles | bool | true | Set false to use your own CSS. | | hiddenInputName | string | null | Auto-create <input type="hidden" name="..."> synced with selections. | | hiddenInputSelector | string | null | Sync to an existing hidden input instead. | | hiddenInputJoin | string | ',' | ',' (CSV), 'json', or any separator. |


API methods

ms.getValues();              // ['1', '5', '7']
ms.getSelected();            // [{id:'1', name:'Foo'}, ...]
ms.setValues([1, 2, 3]);     // pre-select by id
ms.setValues([{id:1, name:'Foo'}, ...]);  // or with names (skips resolveNames)
ms.clear();                  // unselect all
ms.reload();                 // reset search and reload
ms.open();
ms.close();
ms.destroy();                // teardown, remove DOM, abort pending requests

Static helpers

// Auto-init by data attributes (no JS config needed)
LazySelect.autoInit();

// Override defaults globally
LazySelect.setDefaults({ pageSize: 25, searchDebounce: 200 });

Recipes

Sync to a hidden input for plain form submission

<form action="/search" method="get">
  <div id="cat" data-ls-url="/api/categories" data-ls-name="categories"></div>
  <button>Search</button>
</form>
<script>LazySelect.autoInit();</script>

The form will submit ?categories=1,5,7 automatically — zero JavaScript config.

Send CSRF tokens (Django, Rails, etc)

new LazySelect('#x', {
  url: '/api/items',
  method: 'POST',
  headers: { 'X-CSRFToken': getCookie('csrftoken') }
});

Pre-select with names already known

new LazySelect('#x', {
  url: '/api/items',
  initialValues: [{id: 5, name: 'Engineering'}, {id: 12, name: 'Sales'}]
});

Pre-select by id only, fetch names lazily

new LazySelect('#x', {
  url: '/api/items',
  initialValues: [5, 12],
  resolveNames: (ids) => fetch('/api/items/lookup?ids=' + ids.join(','))
                            .then(r => r.json())
});

Dependent dropdowns

const country = new LazySelect('#country', {
  url: '/api/countries',
  onChange: () => city.reload()
});
const city = new LazySelect('#city', {
  url: '/api/cities',
  getExtraParams: () => ({ country: country.getValues().join(',') })
});

Auto-init from HTML (no JS config)

<div data-ls-url="/api/categories" data-ls-key="cat"
     data-ls-placeholder="Pick categories" data-ls-name="cat_ids"></div>
<div data-ls-url="/api/users" data-ls-key="usr" data-ls-page-size="20"></div>

<script>LazySelect.autoInit();</script>

Custom styling

Either set autoInjectStyles: false and write your own CSS using the same class names, or just override what you need:

.ls-dropdown { border-color: #6610f2; }
.ls-item:hover { background: #e7f1ff; }
.ls-search:focus { border-color: #6610f2; box-shadow: 0 0 0 .2rem rgba(102,16,242,.15); }

CSS class names: .ls-container, .ls-search, .ls-dropdown, .ls-items, .ls-item, .ls-loading, .ls-empty, .ls-error.


How is this different from select2 / choices.js / chosen?

Those are excellent libraries, but they all expect the full options list to be available client-side, or use ad-hoc AJAX patterns layered on top. LazySelect is built around server-side pagination from the start: every option you see came from a paged API request, search runs server-side, and selections persist correctly even when the user has only ever seen a fraction of the available items.

If your data fits in the page, use <select multiple> or one of the libraries above. If you have thousands of rows, use this.


Browser support

Modern evergreen browsers. Uses fetch, Promise, AbortController, Object.assign, and dataset — IE11 is not supported.

Size

| File | Size | |---|---| | lazyselect.js | ~18 KB | | lazyselect.min.js | ~11 KB | | Minified + gzipped | ~3.7 KB |

Contributing

Bug reports and pull requests are welcome at github.com/agentnova/lazyselect. For substantial changes, please open an issue first to discuss.

License

MIT © agentnova