react ~12 KB 0 deps v0.4.0 β†— GitHub β†—

@mshafiqyajid/react-select

Headless select hook and styled dropdown for React. Single and multi-select, searchable, clearable, chips for multi-value, controllable placement (auto/top/bottom) with flip and offset, portal positioning, full keyboard navigation.

Playground #

Props
TSX
import { SelectStyled } from "@mshafiqyajid/react-select/styled";
import "@mshafiqyajid/react-select/styles.css";

<SelectStyled
  items={items}
  value={value}
  onChange={setValue}
/>

Grouped items

Grouped single select

Grouped multi-select with renderItem

Render props

Custom renderTrigger

Async loadOptions

Async loadOptions (mock 600ms delay)

Install #

npm install @mshafiqyajid/react-select

Quick start #

import { SelectStyled } from "@mshafiqyajid/react-select/styled";
import "@mshafiqyajid/react-select/styles.css";

const items = [
  { value: "react",  label: "React" },
  { value: "vue",    label: "Vue" },
  { value: "svelte", label: "Svelte" },
];

const [value, setValue] = useState("");

<SelectStyled
  items={items}
  value={value}
  onChange={setValue}
  placeholder="Pick a framework"
/>

Multi-select #

const [values, setValues] = useState<string[]>([]);

<SelectStyled
  items={items}
  value={values}
  onChange={setValues}
  multiple
  clearable
/>

Searchable #

<SelectStyled items={items} value={value} onChange={setValue} searchable />

Async loadOptions #

<SelectStyled
  items={[]}                       // seeds + provides labels for selected values
  value={value}
  onChange={setValue}
  loadOptions={async (query) => {
    const res = await fetch(`/api/users?q=${encodeURIComponent(query)}`);
    return res.json(); // SelectItem[]
  }}
  debounceMs={300}                 // default
  loadingText="Searching…"
  emptyText="No matches"
  errorText="Couldn't load β€” try again"
/>

Debounced on searchValue; stale requests cancelled via AbortController. Hook exposes isLoading + loadError; listbox lands aria-busy="true" while in flight.

Headless #

import { useSelect } from "@mshafiqyajid/react-select";

const items = [
  { value: "react", label: "React" },
  { value: "vue",   label: "Vue" },
];
const [value, setValue] = useState("react");

const { triggerProps, listboxProps, getItemProps, isOpen } = useSelect({
  items,
  value,
  onChange: (v) => setValue(v as string),
});

return (
  <>
    <button {...triggerProps}>{value || "Select…"}</button>
    {isOpen && (
      <ul {...listboxProps} className="listbox">
        {items.map((item) => (
          <li key={item.value} {...getItemProps(item)}>{item.label}</li>
        ))}
      </ul>
    )}
  </>
);

API #

PropTypeDefaultDescription
itemsSelectItem[]β€”Array of { value, label, disabled? }
valuestring | string[]β€”Selected value(s)
onChange(v: string | string[]) => voidβ€”Called on selection change
placeholderstring"Select…"Placeholder text
multiplebooleanfalseAllow multiple selections
searchablebooleanfalseShow search input in dropdown
clearablebooleanfalseShow clear button
size"sm" | "md" | "lg""md"Size variant
tone"neutral" | "primary" | "success" | "danger""neutral"Color tone
disabledbooleanfalseDisable the select
placement"auto" | "top" | "bottom""auto"Listbox side. "auto" picks bottom unless there isn't room.
offsetnumber4Gap between trigger and listbox
collisionPaddingnumber8Viewport edge margin used by flip
flipbooleantrueAuto-flip when there isn't room on the chosen side
strategy"absolute" | "fixed""absolute"Positioning strategy

data-placement="top" | "bottom" lands on the dropdown so you can flip arrow direction or radius. Listbox width still tracks the trigger.

Edit this page on GitHub