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

@rmnddesign/multi-select

v1.0.1

Published

A tiny type-safe React hook for managing multi-select checkbox state

Downloads

179

Readme

@rmnddesign/multi-select

A tiny type-safe React hook for managing multi-select checkbox state.

The Problem

When you have a list of checkboxes where users can select multiple items, you end up writing the same boilerplate code over and over:

const [selected, setSelected] = useState<string[]>([])

const toggleItem = (item: string) => {
  setSelected(prev =>
    prev.includes(item)
      ? prev.filter(x => x !== item)  // remove if already selected
      : [...prev, item]               // add if not selected
  )
}

const isSelected = (item: string) => selected.includes(item)

This hook does all of that for you.

Installation

npm install @rmnddesign/multi-select

Quick Start

import { useMultiSelect } from "@rmnddesign/multi-select"

function FruitPicker() {
  const { selected, toggle, isSelected } = useMultiSelect<string>()

  const fruits = ["apple", "banana", "cherry", "mango"]

  return (
    <div>
      <h3>Pick your favorite fruits:</h3>

      {fruits.map((fruit) => (
        <label key={fruit} style={{ display: "block" }}>
          <input
            type="checkbox"
            checked={isSelected(fruit)}
            onChange={() => toggle(fruit)}
          />
          {fruit}
        </label>
      ))}

      <p>You selected: {selected.length === 0 ? "nothing yet" : selected.join(", ")}</p>
    </div>
  )
}

What's happening here:

  • useMultiSelect<string>() creates a new multi-select state for string values
  • isSelected(fruit) returns true if that fruit is currently selected
  • toggle(fruit) adds the fruit if not selected, or removes it if already selected
  • selected is the array of all currently selected fruits

Common Examples

Pre-select some items

Pass an array of initially selected items:

// "banana" and "mango" will be checked by default
const { selected, toggle, isSelected } = useMultiSelect(["banana", "mango"])

Limit how many items can be selected

Use the max option to set a maximum:

// Users can only select up to 3 items
const { selected, toggle, isSelected } = useMultiSelect<string>([], {
  max: 3
})

// When 3 items are selected, toggle() won't add more items
// (but it will still remove items if you click a selected one)

Require at least one selection

Use the min option to prevent deselecting below a threshold:

// Users must keep at least 1 item selected
const { selected, toggle, isSelected } = useMultiSelect(["apple"], {
  min: 1
})

// If only 1 item is selected, clicking it won't deselect it

Do something when selection changes

Use the onChange callback:

const { selected, toggle, isSelected } = useMultiSelect<string>([], {
  onChange: (newSelected) => {
    console.log("Selection changed:", newSelected)
    // Maybe save to localStorage, send to server, etc.
  }
})

Select All / Clear All buttons

function MyComponent() {
  const { selected, toggle, isSelected, selectAll, clear } = useMultiSelect<string>()

  const allOptions = ["apple", "banana", "cherry", "mango"]

  return (
    <div>
      <button onClick={() => selectAll(allOptions)}>Select All</button>
      <button onClick={() => clear()}>Clear All</button>

      {allOptions.map((option) => (
        <label key={option}>
          <input
            type="checkbox"
            checked={isSelected(option)}
            onChange={() => toggle(option)}
          />
          {option}
        </label>
      ))}
    </div>
  )
}

Working with objects (not just strings)

You can use any type, not just strings. For objects, make sure you're using the same reference:

type User = { id: number; name: string }

function UserPicker() {
  // Define users outside or useMemo to keep stable references
  const users: User[] = [
    { id: 1, name: "Alice" },
    { id: 2, name: "Bob" },
    { id: 3, name: "Charlie" },
  ]

  const { selected, toggle, isSelected } = useMultiSelect<User>()

  return (
    <div>
      {users.map((user) => (
        <label key={user.id}>
          <input
            type="checkbox"
            checked={isSelected(user)}
            onChange={() => toggle(user)}
          />
          {user.name}
        </label>
      ))}

      <p>Selected IDs: {selected.map(u => u.id).join(", ")}</p>
    </div>
  )
}

Important: For objects, isSelected() uses reference equality (===). This means the exact same object reference must be used. If you're fetching data from an API, consider using IDs (strings/numbers) instead of full objects.

Using IDs instead of objects (recommended for API data)

type User = { id: number; name: string }

function UserPicker() {
  const [users, setUsers] = useState<User[]>([])

  // Store just the IDs, not the full objects
  const { selected, toggle, isSelected } = useMultiSelect<number>()

  useEffect(() => {
    fetch("/api/users")
      .then(res => res.json())
      .then(setUsers)
  }, [])

  return (
    <div>
      {users.map((user) => (
        <label key={user.id}>
          <input
            type="checkbox"
            checked={isSelected(user.id)}  // check by ID
            onChange={() => toggle(user.id)}  // toggle by ID
          />
          {user.name}
        </label>
      ))}

      <p>Selected IDs: {selected.join(", ")}</p>

      {/* If you need the full objects: */}
      <p>Selected users: {users.filter(u => selected.includes(u.id)).map(u => u.name).join(", ")}</p>
    </div>
  )
}

Full API Reference

The hook

const result = useMultiSelect<T>(initialSelected?, options?)

Parameters:

| Parameter | Type | Default | Description | |-----------|------|---------|-------------| | initialSelected | T[] | [] | Items that should be selected when the component first renders | | options | object | {} | Configuration options (see below) |

Options:

| Option | Type | Description | |--------|------|-------------| | max | number | Maximum number of items that can be selected. When reached, toggle() and select() won't add more. | | min | number | Minimum number of items that must stay selected. When reached, toggle() and deselect() won't remove more. | | onChange | (selected: T[]) => void | Function called whenever the selection changes. Receives the new array of selected items. |

What you get back

| Property | Type | Description | |----------|------|-------------| | selected | T[] | Array of currently selected items | | toggle(item) | (item: T) => void | Add item if not selected, remove if selected | | isSelected(item) | (item: T) => boolean | Check if an item is currently selected | | select(item) | (item: T) => void | Add an item (does nothing if already selected) | | deselect(item) | (item: T) => void | Remove an item (does nothing if not selected) | | selectAll(items) | (items: T[]) => void | Add multiple items at once | | clear() | () => void | Remove all selected items | | setSelected(items) | (items: T[]) => void | Replace the entire selection with a new array |

TypeScript

The hook is fully type-safe. Specify your type in angle brackets:

// For strings
const { selected } = useMultiSelect<string>()
// selected is string[]

// For numbers
const { selected } = useMultiSelect<number>()
// selected is number[]

// For custom types
type Tag = { id: string; label: string }
const { selected } = useMultiSelect<Tag>()
// selected is Tag[]

If you pass initial values, TypeScript will infer the type automatically:

// TypeScript knows this is useMultiSelect<string>
const { selected } = useMultiSelect(["apple", "banana"])

License

MIT