react-dropdown: a select-only combobox with no runtime dependencies
🔽 A dead simple dropdown component for React
At a glance
- What is it?
- react-dropdown 2.0 is a TypeScript dropdown for React 18 and 19 that takes over where a native select is too hard to style but a full select framework is too much, and version 2 rebuilt it around a combobox pattern with an exports map and a hidden form input.
- Who is it for?
- Adopt react-dropdown when you need a styled select that a native element cannot deliver, your app is on React 18 or 19, and you want keyboard support and form participation without pulling in a select library. Do not adopt it if you need a multi-select or a combobox that filters as the user types, since the trigger is deliberately select-only, and check your CSS if you styled the 1.x version, because the control is now a button.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Positioned between the native select and a full framework
The project describes itself as a lightweight, accessible dropdown component for React 18 and 19, and the positioning is precise: it is for the common case where a native select is too difficult to style, but a large select framework would be excessive. That sentence is the whole product definition. You are not getting a data grid, not a multi-select engine, not a virtualised list. You are getting one control that looks like what your designer drew and behaves like a form field. The audience is front end developers who have already tried every popular select library and found each one heavy for a single dropdown, and who want TypeScript types without a runtime dependency. The README notes that the library supports single selection, option groups, controlled and uncontrolled state, custom rendering, keyboard navigation, and TypeScript, and the package manifest confirms the zero dependency claim by listing only peer dependencies on react and react-dom, with everything else under devDependencies for the build and test tooling.
The trigger is a combobox, not a listbox
The accessibility model is the part that distinguishes version 2. The README states that the trigger implements a select-only combobox with a listbox popup, and the keyboard contract is spelled out key by key: Enter or Space to open and select, ArrowDown and ArrowUp to open and move through options, Home and End to jump to the first or last enabled option, Escape to close, and Tab to close while letting normal page navigation continue. Disabled options, named groups, selected state and active-descendant announcements are all handled. The select-only qualifier is the important part. A combobox normally means a text input that filters a list, and this component deliberately does not offer that. If your dropdown needs to let people type to narrow the choices, this is the wrong library, and the README is clear enough about that boundary that you can tell before you install it.
Install, import the stylesheet, and pass option objects
Installation is a single npm command, and the component ships a default stylesheet you import alongside it, so the control looks reasonable before you write any CSS.
npm install react-dropdownThe component and its type are imported from the package root, and the stylesheet is a separate import. The example below shows the full shape: an array of option objects with a value and a label, one of which is a group with its own nested items.
import Dropdown, { type Option } from 'react-dropdown'
import 'react-dropdown/style.css'
const options = [
{ value: 'one', label: 'One' },
{ value: 'two', label: 'Two' },
{
type: 'group' as const,
name: 'More',
items: [
{ value: 'three', label: 'Three' },
{ value: 'four', label: 'Four', disabled: true },
],
},
]
function Example() {
const handleChange = (option: Option) => {
console.log(option.value)
}
return (
<Dropdown
aria-label="Number"
options={options}
onChange={handleChange}
placeholder="Select an option"
/>
)
}Primitive options are also accepted, so a plain array of strings works, and string, number and boolean values are supported. Labels may be any ReactNode, which means you can put an image or a formatted price in the option row rather than only a text label.
Controlled and uncontrolled state, including a hidden form input
Selection state and open state can be controlled independently or left alone. You pass value and onChange to control the selection, open and onOpenChange to control the popup, and the README notes that null clears a controlled value while defaultValue and defaultOpen set the initial state for the uncontrolled case. The subtlety is that 0 and false are valid values, so a truthiness check in your own code will misread them.
const [value, setValue] = useState<Option | null>(null)
const [open, setOpen] = useState(false)
<Dropdown
options={options}
value={value}
onChange={setValue}
open={open}
onOpenChange={setOpen}
/>For forms, passing name adds a hidden input carrying the selected value so native form submission picks it up, and passing form associates that hidden input with a form by ID. That is the piece most hand written dropdowns get wrong, and here it is a prop rather than an exercise.
<Dropdown name="country" options={countries} defaultValue="au" />Custom rendering, class names, and the styling escape hatches
Option contents are replaceable through renderOption, which receives the option and a state object with active and selected flags, so you can mark the selected row and the highlighted row yourself. The same state drives optionClassName, which takes the option and state and returns a class name or undefined, so you do not have to duplicate the logic to style it. Options may also carry their own className and a data object, which is a convenient place to hang analytics identifiers.
<Dropdown
options={options}
renderOption={(option, { active, selected }) => (
<span>
{selected ? '✓ ' : ''}
{option.label}
{active ? ' ←' : ''}
</span>
)}
optionClassName={(_option, state) => (state.active ? 'my-active-option' : undefined)}
/>The default stylesheet keeps familiar class names, including .Dropdown-root, .Dropdown-control, .Dropdown-placeholder, .Dropdown-arrow-wrapper, .Dropdown-arrow, .Dropdown-menu, .Dropdown-group, .Dropdown-title, .Dropdown-option and .Dropdown-noresults, plus state classes .is-open, .is-selected, .is-active and .is-disabled. Seven styling props let you replace any of them: baseClassName, className, controlClassName, placeholderClassName, menuClassName, optionClassName and arrowClassName. Setting baseClassName swaps the Dropdown prefix out entirely, so you can supply a full set of your own class names.
Version 2 changed the DOM, the packaging and the semantics
The migration notes are unusually blunt, which is good, because this is a breaking release. React 18 or 19 is required. The package ships both ESM and CommonJS through an exports map, which the manifest confirms along with a style field pointing at style.css. The trigger is now a focusable button with role combobox, so CSS that assumed .Dropdown-control was a div needs adjusting, and the README advises preferring class selectors over element selectors because the menu now uses semantic ul and li elements. onFocus fires when the trigger actually receives focus. onChange returns the complete normalised option, including className, disabled and data when present, rather than a bare value. value with null clears the selection, and 0 and false count as values. The old global .Dropdown-disabled class is now applied directly to the disabled control. And optionClassName, renderOption, open and onOpenChange replace the workarounds that 1.x users had been applying by hand, which tells you the API was reshaped around what people were doing anyway.
Development setup and the shape of the repository
The build and test commands are all npm scripts, and the README lists them as the development workflow for contributors.
npm install
npm run dev # local example
npm test # interaction tests
npm run typecheck
npm run build
npm run check # all release checksThe repository layout matches that workflow: src/ holds the component, test/ holds the interaction tests, example/ contains a small demo driven by vite.demo.config.ts, and there are separate Vite configurations for the library build and the demo build, plus a vitest.config.ts. The test stack is testing-library for DOM, React and user event, running on jsdom, so the tests exercise real user interactions rather than snapshotting markup. TypeScript 5.9 and Vite 8 are the current toolchain. The package is MIT licensed, there is a CHANGELOG, and the only published release is v2.0.0 from 2026-07-11, with the last push to the repository on 2026-10-16, so the 2.x line is where any fixes will land.
Editorial conclusion
Adopt react-dropdown when you need a styled select that a native element cannot deliver, your app is on React 18 or 19, and you want keyboard support and form participation without pulling in a select library. Do not adopt it if you need a multi-select or a combobox that filters as the user types, since the trigger is deliberately select-only, and check your CSS if you styled the 1.x version, because the control is now a button. Verify first that your option data maps onto the object shape the 2.0 API expects, and read the 1.x to 2.0 migration list before upgrading an existing dropdown.
Frequently asked questions
How do I install react-dropdown and use it with TypeScript?
Run npm install react-dropdown, then import the component and its type from the package root along with the stylesheet: import Dropdown, { type Option } from 'react-dropdown' and import 'react-dropdown/style.css'. The package ships no runtime dependencies beyond React itself.
Does react-dropdown support filtering as the user types?
No. The README describes the trigger as a select-only combobox with a listbox popup, and the keyboard support covers opening, moving through options, Home, End, Escape and Tab, with no text filtering behaviour described.
How do I make react-dropdown work with a native HTML form?
Pass a name prop and the component renders a hidden input holding the selected value, so native form submission includes it. Passing a form prop associates that hidden input with a form by ID.
What breaks when I upgrade react-dropdown from 1.x to 2.0?
The trigger is now a focusable button with role combobox, so CSS that assumed .Dropdown-control was a div needs adjusting, and the menu uses ul and li elements so class selectors are preferred. React 18 or 19 is now required, onChange returns the full normalised option, and value null clears a controlled selection.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/fraserxu-react-dropdown)Was this useful?