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

@glook/downshift-primitives

v0.9.0

Published

Headless compound components for combobox, multi-combobox and select, built on downshift

Downloads

80

Readme

@glook/downshift-primitives

Headless compound components for combobox, multi-combobox and select, built on downshift.

No styles, no markup opinions. Every part is a slot you render yourself; state is exposed through data-* attributes. Async loading, cursor pagination, debounced filtering and floating positioning are handled for you.

npm install @glook/downshift-primitives

Peer deps: react / react-dom ^16.8 || ^17 || ^18.

Usage

import {Arrow, Combobox, Input, Listbox, ListBoxItems, Option, OptionState, SelectedItem, Trigger} from '@glook/downshift-primitives';

<Combobox<City, number>
    getItems={getCities}
    itemToString={(city) => city?.name ?? ''}
    renderSelectedItem={(city) => <span>{city.name}</span>}
    debounceTime={300}
>
    <Trigger asChild>
        <span className="trigger">
            <Input asChild placeholder="Start typing a city">
                <input className="input" />
            </Input>
            <SelectedItem className="selectedItem" />
            <Arrow className="arrow">▾</Arrow>
        </span>
    </Trigger>

    <Listbox asChild>
        <ul className="listbox">
            <ListBoxItems<City> getOptionValue={(city) => city.id}>
                {({values}) => (
                    <>
                        {values.map(({rawValue}, index) => (
                            <Option asChild key={rawValue.id} rawValue={rawValue} index={index}>
                                <li className="option">{rawValue.name}</li>
                            </Option>
                        ))}
                        <OptionState type="loading" asChild>
                            <li>Loading…</li>
                        </OptionState>
                        <OptionState type="noResults" asChild>
                            <li>Nothing found</li>
                        </OptionState>
                    </>
                )}
            </ListBoxItems>
        </ul>
    </Listbox>
</Combobox>;

Every component is exported twice — namespaced (DownshiftCombobox) and short (Combobox). Pick one and stay consistent.

Loading items

All three roots take a single getItems function. It is called on open, on filter change and on scroll to the bottom:

const getCities: DownshiftGetItemsFn<City, number> = async ({filterText, signal}, cursor) => {
    const page = cursor ?? 0;
    const res = await fetch(`/cities?q=${filterText ?? ''}&page=${page}`, {signal});
    const items = await res.json();

    // returning a cursor enables infinite scroll; omit it to signal the last page
    return {items, cursor: items.length ? page + 1 : undefined};
};

Items are cleared when the menu closes and re-fetched on open. In-flight requests are aborted through signal.

Roots

| Root | Selection | Required props | | --- | --- | --- | | Combobox | single, with text input | getItems, itemToString, renderSelectedItem | | MultiCombobox | multiple, with chips | getItems, getOptionValue, renderSelectedItem, selectedItems, onChange | | Select | single, no text input | getItems, renderSelectedItem |

MultiCombobox is controlled: it holds no selection of its own. Selected items are hidden from the list, compared by getOptionValue.

Anything else from downshift's useCombobox / useSelect (onSelectedItemChange, stateReducer, …) is passed straight through.

Parts

Trigger, Input, Label, Listbox, ListBoxItems, Option, OptionState, SelectedItem, SelectedItems, Chip, ChipRemove, Clear, Arrow, LoadingIndicator.

Parts that read the root: Trigger, Clear and SelectedItem render the right variant automatically. Parts that do not apply render nothing — Input inside a Select, Placeholder inside a Combobox.

OptionState takes type: 'loading' | 'loadingMore' | 'noResults' | 'error' and decides on its own whether to show.

Most parts accept asChild and merge their props into your element, so you keep full control of the markup.

Styling

There is no CSS. Hook onto the data attributes:

data-is-open, data-is-loading, data-loading-state, data-is-selected, data-is-active, data-is-disabled, data-is-hovered, data-is-focused, data-has-item, data-has-no-items, data-has-error, data-has-selected-item, data-has-input-value.

.option[data-is-active] { background: #eee; }
.option[data-is-selected] { font-weight: 600; }

The listbox is positioned with floating-ui and exposes --list-box-reference-width and --list-box-available-height as CSS variables. Tune it with dropdownMenuFloatingOptions:

<Combobox dropdownMenuFloatingOptions={{placement: 'bottom-start', offset: 5, applyWidth: true}} />

applyWidth (default true) also hard-sets the listbox width / maxWidth from the trigger.

Development

npm run storybook   # component catalogue, the only way to run things by hand
npm run build       # dist/index.{js,cjs,d.ts}
npm run typecheck

License

MIT © Andrey Polyakov