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

@alsocoder/apna-select

v0.2.3

Published

A flexible React select component with single/multi select, search, async API loading, and CSS customization.

Readme

@alsocoder/apna-select

A flexible React select component with single/multi select, search, and async API loading.

Features

  • Single and multi select
  • Client-side and server-side search
  • Controlled and uncontrolled modes
  • Async data loading via url or loadOptions
  • Native form support with hidden inputs (name prop)
  • Fully customizable via className, classNames, and CSS variables
  • Built on Radix UI Popover + Checkbox
  • Keyboard navigation (Arrow keys, Enter, Escape, Home, End)
  • Cross-browser compatible (Chrome, Firefox, Safari, Edge, iOS)

Installation

npm install @alsocoder/apna-select

Peer dependencies:

npm install react react-dom

Setup

Import the stylesheet once in your app entry:

import { ApnaSelect } from "@alsocoder/apna-select"
import "@alsocoder/apna-select/styles.css"

Basic Usage

Static single select

import { ApnaSelect } from "@alsocoder/apna-select"
import "@alsocoder/apna-select/styles.css"

const options = [
  { value: "active", label: "Active" },
  { value: "inactive", label: "Inactive" },
]

export function StatusSelect() {
  const [status, setStatus] = useState("")

  return (
    <ApnaSelect
      options={options}
      value={status}
      onValueChange={setStatus}
      placeholder="Select status"
    />
  )
}

Multi select

<ApnaSelect
  multiple
  options={options}
  value={selected}
  onValueChange={setSelected}
  placeholder="Select skills"
/>

Theming with CSS Variables

Override colors globally without touching component code:

:root {
  --apna-select-trigger-border: #3b82f6;
  --apna-select-trigger-bg: #eff6ff;
  --apna-select-option-hover: #dbeafe;
  --apna-select-primary: #2563eb;
}

Available CSS variables

| Variable | Default fallback | Description | |----------|------------------|-------------| | --apna-select-trigger-bg | --input | Trigger background | | --apna-select-trigger-border | --border | Trigger border | | --apna-select-trigger-text | --foreground | Selected text color | | --apna-select-trigger-placeholder | --muted-foreground | Placeholder color | | --apna-select-content-bg | --popover | Dropdown background | | --apna-select-content-text | --popover-foreground | Dropdown text | | --apna-select-content-border | --border | Dropdown border | | --apna-select-option-hover | --muted | Option hover background | | --apna-select-option-selected | --muted | Selected option background | | --apna-select-option-highlighted | primary tint | Keyboard/mouse highlight | | --apna-select-muted | --muted-foreground | Muted text/icons | | --apna-select-ring | --ring | Focus ring color | | --apna-select-primary | --primary | Checkbox/check color | | --apna-select-destructive | --destructive | Error text color | | --apna-select-radius | 0.5rem | Border radius | | --apna-select-font-size | 0.875rem | Font size |

Customization

Level 1: Shortcut props

<ApnaSelect
  options={options}
  className="my-trigger"
  contentClassName="my-dropdown"
/>

Level 2: Granular classNames

<ApnaSelect
  options={options}
  classNames={{
    trigger: "rounded-xl",
    option: "font-bold",
  }}
/>

Level 3: CSS variables (see above)

Available classNames slots

| Slot | Default class | Description | |------|---------------|-------------| | field | apna-select-field | Trigger wrapper | | trigger | apna-select-trigger | Main trigger | | triggerLabel | apna-select-trigger-label | Selected label text | | triggerBadges | apna-select-badges | Badge container (multi) | | badge | apna-select-badge | Single badge | | badgeLabel | apna-select-badge-label | Badge text | | badgeRemove | apna-select-badge-remove | Badge remove button | | clear | apna-select-clear | Clear button | | chevron | apna-select-chevron | Chevron icon | | content | apna-select-content | Popover panel | | groupLabel | apna-select-group-label | Option group heading | | searchContainer | apna-select-search | Search bar wrapper | | searchInput | apna-select-search-input | Search input | | searchIcon | apna-select-search-icon | Search icon | | list | apna-select-list | Options scroll container | | option | apna-select-option | Each option button | | optionSelected | apna-select-option--selected | Selected option state | | optionHighlighted | apna-select-option--highlighted | Keyboard/mouse highlight | | optionLabel | apna-select-option-label | Option label text | | optionCheck | apna-select-option-check | Check icon (single select) | | empty | apna-select-empty | Empty state message | | checkbox | apna-select-checkbox | Multi-select checkbox | | loading | apna-select-loading | Loading state | | error | apna-select-error | Error state | | retryButton | apna-select-retry | Retry button | | spinner | apna-select-spinner | Loading spinner |

Option groups

Add a group field on options:

const options = [
  { value: "in", label: "India", group: "Asia" },
  { value: "jp", label: "Japan", group: "Asia" },
  { value: "us", label: "United States", group: "Americas" },
]

<ApnaSelect options={options} placeholder="Select country" />

Clearable + badge multi-select

<ApnaSelect clearable options={options} value={value} onValueChange={setValue} />

<ApnaSelect
  multiple
  multipleDisplay="badges"
  clearable
  options={options}
  value={selected}
  onValueChange={setSelected}
/>

When theming with CSS variables, apply the same theme class to classNames.content as well — the dropdown renders in a portal.

Async Usage

Fetch once (load on open)

<ApnaSelect
  url="/api/cities"
  mapResponse={(data) =>
    data.map((city) => ({ value: city.id, label: city.name }))
  }
  placeholder="Select city"
/>

Server-side search

<ApnaSelect
  url="/api/users"
  searchMode="server"
  searchParam="q"
  debounceMs={300}
  minSearchLength={2}
  mapResponse={(data) => data.users}
  placeholder="Search users"
/>

Custom fetcher (axios, auth, etc.)

<ApnaSelect
  searchMode="server"
  debounceMs={400}
  loadOptions={async (search, { signal }) => {
    const res = await fetch(`/api/products?q=${search}`, { signal })
    const data = await res.json()
    return data.items.map((item) => ({
      value: item.slug,
      label: item.title,
    }))
  }}
/>

Standalone hook

import { ApnaSelect, useApnaSelectOptions } from "@alsocoder/apna-select"

const { options, loading, error, retry } = useApnaSelectOptions({
  url: "/api/states",
  mapResponse: (data) => data.states,
})

return (
  <ApnaSelect
    options={options}
    loading={loading}
    error={error}
    onRetry={retry}
  />
)

Props

Core

| Prop | Type | Default | Description | |------|------|---------|-------------| | options | ApnaSelectOption[] | — | Static options (required in static mode) | | multiple | boolean | false | Enable multi select | | value | string \| string[] | — | Controlled value | | defaultValue | string \| string[] | — | Uncontrolled default | | onValueChange | function | — | Change handler | | placeholder | string | "Select option" | Placeholder text | | searchPlaceholder | string | "Search..." | Search input placeholder | | emptyText | string | "No results found." | Empty state text | | minSearchText | string | auto | Prompt before server search | | clearable | boolean | false | Show clear button when selected | | clearLabel | string | "Clear selection" | Clear button aria-label | | multipleDisplay | "text" \| "badges" | "text" | Multi-select display mode | | resolveLabel | function | — | Resolve label for controlled values | | searchThreshold | number | 8 | Min options before search shows | | disabled | boolean | false | Disable the select | | name | string | — | Hidden input name for forms | | id | string | — | HTML id attribute | | className | string | — | Trigger class shortcut | | contentClassName | string | — | Content class shortcut | | classNames | object | — | Granular class overrides | | filterFn | function | label match | Client-side filter function | | icons | object | inline SVGs | Custom chevron/search/check icons |

Async

| Prop | Type | Default | Description | |------|------|---------|-------------| | url | string | — | API endpoint | | method | "GET" \| "POST" | "GET" | HTTP method | | headers | object | — | Request headers | | body | object | — | POST request body | | searchMode | "client" \| "server" | "client" | Search behavior | | searchParam | string | "search" | Query param for server search | | debounceMs | number | 300 | Debounce for server search | | minSearchLength | number | 0 | Min chars before API call | | mapResponse | function | auto-detect | Transform API response | | loadOptions | function | — | Custom async loader | | fetchOnMount | boolean | true | Fetch when dropdown opens | | cacheKey | string | url-based | Cache key | | loading | boolean | auto | Manual loading state | | error | string \| Error | auto | Manual error state | | onRetry | function | auto | Retry handler | | loadingText | string | "Loading..." | Loading message | | errorText | string | — | Custom error message |

Native Form Integration

<form onSubmit={handleSubmit}>
  <ApnaSelect
    name="service"
    options={options}
    value={service}
    onValueChange={setService}
  />
  <button type="submit">Submit</button>
</form>

react-hook-form Example

import { Controller, useForm } from "react-hook-form"
import { ApnaSelect } from "@alsocoder/apna-select"

function MyForm() {
  const { control, handleSubmit } = useForm()

  return (
    <form onSubmit={handleSubmit(console.log)}>
      <Controller
        name="status"
        control={control}
        render={({ field }) => (
          <ApnaSelect
            options={options}
            value={field.value}
            onValueChange={field.onChange}
          />
        )}
      />
    </form>
  )
}

Keyboard Navigation

| Key | Action | |-----|--------| | ArrowDown / ArrowUp | Move highlight / open dropdown | | Enter / Space | Select highlighted option / open dropdown | | Home / End | Jump to first / last option | | Escape | Close dropdown | | Tab | Close dropdown and move focus |

Works on trigger button and search input. Mouse hover syncs with keyboard highlight.

Browser Compatibility

Tested and supported on modern browsers:

| Browser | Minimum Version | |---------|-----------------| | Chrome | 90+ | | Firefox | 90+ | | Safari | 14+ | | Edge | 90+ | | iOS Safari | 14+ | | Samsung Internet | 15+ |

CSS includes fallbacks for older engines:

  • Solid color fallbacks before color-mix()
  • -webkit- prefixes for flexbox and animations
  • prefers-reduced-motion support (disables animations)
  • -webkit-overflow-scrolling: touch for iOS scroll

Requires React 18+. Internet Explorer is not supported.

Exports

import {
  ApnaSelect,
  useApnaSelectOptions,
  defaultClassNames,
  defaultMapResponse,
  clearOptionsCache,
} from "@alsocoder/apna-select"

Changelog

v0.2.3

  • onBlur prop for react-hook-form integration
  • fieldError / fieldErrorText for field-level validation (separate from async fetch errors)
  • label, required, trigger error border, and aria-invalid

v0.2.2

  • DismissableLayerBranch on portaled popover — works inside Radix Dialog / ApnaModal
  • pointer-events: auto on popover for Dialog body scroll-lock compatibility
  • @radix-ui/react-dismissable-layer added as explicit dependency

v0.2.1

  • Popover z-index uses --apna-overlay-nested-z-popover for ApnaModal compatibility

v0.2.0

  • clearable prop with clear button
  • multipleDisplay="badges" for chip-style multi-select
  • Option groups via group field on options
  • resolveLabel for controlled server-search values
  • minSearchText for async server search prompt state
  • Selected label cache for async server search
  • Keyboard, scroll, and async UX fixes

v0.1.0

  • Initial release
  • Single/multi select with search
  • Async API loading (url, loadOptions, useApnaSelectOptions)
  • CSS variables for theming
  • Customizable via className, classNames, and CSS variables
  • Keyboard navigation and accessibility improvements
  • Cross-browser CSS with fallbacks and reduced-motion support

Development

npm install
npm run build
cd playground && npm install && npm run dev

Roadmap

  • Cascading/dependent selects (composition pattern)

License

MIT