A lightweight, flexible, and unstyled dropdown component for React applications. Designed for easy integration and customization, with careful consideration for compatibility within the Next.js App Router environment.
👇 Tiny Bundle Size:
- Compressed (Download Size): 10.4 kB
- Uncompressed (Disk Size): 36.2 kB
(For reference, the built JavaScript bundles themselves are approximately ESM: ~2.35 KB, CJS: ~2.85 KB)
Achieved through careful design, no dependencies (only peer for react, react-dom), and minified build processes.
-
🌍 Framework Agnostic: Built in pure React, usable in any React application (Create React App, Vite, Next.js, etc.).
-
🎨 Fully Customizable Styling: Provides CSS class and style props for complete visual control using your preferred CSS methodology (vanilla CSS, CSS Modules, styled-components, Tailwind JIT via class props, etc.). No default visual opinions!
-
✨ Flexible Options: Supports arrays of strings or arrays of objects.
-
⚙️ Powerful Object Option Handling:
- 🔍 Select nested display values using a dot-notation
optionSelector
string. - 🖌️ Provide completely custom rendering for list items using the
optionsRender
prop. - 🏷️ Specify how the selected object value is displayed in the button using the
getOptionLabel
prop.
- 🔍 Select nested display values using a dot-notation
-
🛡️ TypeScript Ready: Includes built-in TypeScript definitions for a smooth development experience.
-
넥 ⚛️ Next.js App Router Compatible: Designed with the App Router's Server/Client Component model in mind, with documented patterns for seamless integration.
-
🌳 Optimized for Size: Benefits from tree-shaking and minimal code footprint.
-
🔄 Event Handling: Includes an
onChange
callback prop that receives the selected option whenever the user makes a selection.
npm
npm install react-dropdown-light
yarn
yarn add react-dropdown-light
pnpm
pnpm add react-dropdown-light
For simple dropdowns with string options, the basic usage is straightforward. This works seamlessly in any React environment, including within a Client Component in Next.js.
JavaScript
// Ensure you are in a Client Component if using hooks/state
// or if this component is rendered within a Server Component boundary.
// Example: MyClientComponent.tsx or a file with "use client";
import React from 'react';
import Dropdown from 'react-dropdown-light';
function MyBasicDropdown() {
const stringOptions = ['Option 1', 'Option 2', 'Option 3'];
const handleSelection = (selectedOption: string) => {
console.log('Selected:', selectedOption);
};
return (
<div>
<h3>String Options Example</h3>
<Dropdown options={stringOptions} onChange={handleSelection}>
Select a value
</Dropdown>
</div>
);
}
export default MyBasicDropdown;
The component provides two main ways to handle object options: using a selector string or providing custom render/label functions.
Use the optionSelector
prop when the value you want to display for an object option is a nested string property. Provide a dot-notation path (e.g., "profile.name"
, "address.street"
).
JavaScript
// Ensure you are in a Client Component context if needed ("use client";)
import React from 'react';
import Dropdown from 'react-dropdown-light';
interface User {
id: number;
profile: {
name: string;
age: number;
};
}
function UserDropdownSelector() {
const userOptions: User[] = [
{ id: 1, profile: { name: 'Alice', age: 30 } },
{ id: 2, profile: { name: 'Bob', age: 25 } },
{ id: 3, profile: { name: 'Charlie', age: 35 } },
];
return (
<div>
<h3>Object Options (Selector) Example</h3>
<Dropdown
options={userOptions}
optionSelector="profile.name" // Use 'profile.name' as the display value
>
Select a User
</Dropdown>
</div>
);
}
export default UserDropdownSelector;
For complete control over how each option is rendered in the dropdown list, or if the label for the selected option requires custom formatting not achievable with optionSelector
, use the optionsRender
and getOptionLabel
props.
optionsRender
: A function that receives an option object and returns the React Node to display for that option in the list. getOptionLabel
: A function that receives the selected option object and returns the string to display in the main dropdown button.
Note: Since these are functions, they must be defined and passed to the Dropdown
component from within a Client Component context.
JavaScript
// Ensure you are in a Client Component context ("use client";)
import React from 'react';
import Dropdown from 'react-dropdown-light';
interface Product {
sku: string;
name: string;
price: number;
}
function ProductDropdownRenderer() {
const productOptions: Product[] = [
{ sku: 'A101', name: 'Laptop', price: 1200 },
{ sku: 'B205', name: 'Keyboard', price: 75 },
{ sku: 'C310', name: 'Mouse', price: 25 },
];
// Function to render each item in the dropdown list
const renderProductOption = (item: Product) => (
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
<span>{item.name}</span>
<strong>${item.price.toFixed(2)}</strong>
</div>
);
// Function to get the label for the selected item in the button
const getProductLabel = (item: Product) => {
return `${item.name} ($${item.price.toFixed(2)})`;
};
const handleProductSelection = (selectedProduct: Product) => {
console.log('Product Selected SKU:', selectedProduct.sku);
};
return (
<div>
<h3>Object Options (Render/Label) Example</h3>
<Dropdown
options={productOptions}
optionsRender={renderProductOption} // Custom rendering for list items
getOptionLabel={getProductLabel} // Custom label for selected item in button
onChange={handleProductSelection} // Handle selection change
>
Select a Product
</Dropdown>
</div>
);
}
export default ProductDropdownRenderer;
This component is intentionally delivered without any default visual styling (besides minimal inline styles for positioning and layout like position: relative
, position: absolute
, z-index
, cursor: pointer
, and button resets). This gives you complete control over its appearance.
You can style the component using standard CSS, CSS Modules, or any other CSS-in-JS library by utilizing the className
and style
props provided for each major part of the component:
-
containerClassName
,containerStyle
: For the maindiv
wrapper (.relative
). -
buttonClassName
,buttonStyle
: For the mainbutton
element. -
listContainerClassName
,listContainerStyle
: For the dropdown listdiv
wrapper (.z-10 .absolute ...
). -
listClassName
,listStyle
: For theul
element whenoptionsRender
is not used. -
listItemClassName
,listItemStyle
: For eachli
orbutton
element within the dropdown list.
Example using CSS Modules:
JavaScript
// In your React Component file (e.g., MyStyledDropdown.tsx)
import React from 'react';
import Dropdown from 'react-dropdown-light';
import styles from './MyStyledDropdown.module.css'; // Import your CSS Module
function MyStyledDropdown() {
const options = ['Apple', 'Banana', 'Cherry'];
return (
<Dropdown
options={options}
containerClassName={styles.myDropdownContainer}
buttonClassName={styles.myDropdownButton}
listContainerClassName={styles.myDropdownListContainer}
listClassName={styles.myDropdownList}
listItemClassName={styles.myDropdownListItem}
>
Choose fruit
</Dropdown>
);
}
export default MyStyledDropdown;
CSS
/* In your CSS Module file (e.g., MyStyledDropdown.module.css) */
.myDropdownContainer {
/* Add container styles here */
/* Position: relative is handled by the component */
display: inline-block; /* Or block, depending on layout */
font-family: 'Arial', sans-serif;
border: 1px solid #ccc; /* Example border around the whole component */
border-radius: 4px;
}
.myDropdownButton {
/* Style the button */
padding: 10px 20px;
background-color: #f9f9f9;
color: #333;
border: none; /* Remove default button border */
cursor: pointer;
font-size: 1rem;
display: flex;
align-items: center;
justify-content: space-between; /* Space between text and arrow */
width: 100%; /* Make button fill container if container is block */
}
.myDropdownButton:hover {
background-color: #e9e9e9;
}
/* You might style the SVG arrow icon too if you add a class to it */
/* .myDropdownButton svg { ... } */
.myDropdownListContainer {
/* Style the dropdown list wrapper */
/* Position: absolute, top, margin-top, z-index are handled by the component */
border: 1px solid #ccc;
border-top: none; /* Example: no top border to connect visually */
background-color: white;
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
border-radius: 0 0 4px 4px;
width: 100%; /* Example: match button width */
box-sizing: border-box; /* Ensure padding/border are included in width */
}
.myDropdownList {
/* Style the UL element */
list-style: none;
margin: 0;
padding: 0; /* Reset default UL padding */
}
.myDropdownListItem {
/* Style each LI or BUTTON in the list */
padding: 10px 20px;
cursor: pointer;
font-size: 1rem;
color: #555;
text-align: left; /* Ensure text alignment */
width: 100%; /* Ensure button fills list item width if using optionsRender */
box-sizing: border-box;
border: none; /* Remove button border if using optionsRender */
background: none; /* Remove button background if using optionsRender */
}
.myDropdownListItem:hover {
background-color: #f0f0f0;
color: #000;
}
/* Add styles for selected item, disabled item, etc. if needed */
You are free to provide styles using any method you prefer. The component just applies the className
and style
props you provide to the relevant DOM elements.
Your Dropdown
component is a Client Component ("use client"
is at the top of its file). This is required because it uses React hooks like useState
to manage its internal state (whether the dropdown is open and which option is selected).
In the Next.js App Router, components are Server Components by default. A key rule is that you cannot pass non-serializable values (like functions, Dates, Classes, Symbols) directly from a Server Component to a Client Component as props.
This impacts the use of the optionsRender
and getOptionLabel
function props if you are trying to define these functions within a Server Component and pass them to the Dropdown
. Attempting this will result in the error: Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".
(Note: "use server"
functions are for Server Actions, which is not the correct pattern for optionsRender
/getOptionLabel
which run on the client).
The Solution: Define Functions within a Client Boundary
If you are using your Dropdown
component within a Server Component, and you need to use the optionsRender
or getOptionLabel
function props, you must define these functions within a Client Component context.
The recommended pattern is to create a Client Wrapper Component:
-
Create a Wrapper: Make a new component file (e.g.,
components/ClientDropdownWrapper.tsx
). Add"use client";
at the very top. -
Define Functions in the Wrapper: Define your
optionsRender
andgetOptionLabel
functions inside this wrapper component. -
Pass Data from Server to Wrapper: Your Server Component will render the
ClientDropdownWrapper
and pass the necessary serializable data (like youroptions
array) as props to the wrapper. -
Pass Data and Functions from Wrapper to Dropdown: The
ClientDropdownWrapper
will then render yourDropdown
component, passing the received serializable data and the client-defined functions to it.
Example of a Client Wrapper Component:
JavaScript
// components/ClientDropdownWrapper.tsx
"use client"; // THIS IS A CLIENT COMPONENT
import React from 'react';
import Dropdown from 'react-dropdown-light'; // Import your published component
// Define types shared between server and client if needed
// This type should represent the structure of the data you pass from the server
interface YourOptionDataType {
id: string;
name: string;
// ... any other serializable properties fetched from the server
}
interface ClientDropdownWrapperProps {
options: YourOptionDataType[]; // This prop is passed from the Server Component (must be serializable)
children?: React.ReactNode; // Pass the button text (children)
// You can also pass styling props here if you want the Server Component
// to control styling via serializable className/style props
containerClassName?: string;
buttonClassName?: string;
// ... etc. for other styling props
}
const ClientDropdownWrapper: React.FC<ClientDropdownWrapperProps> = ({
options,
children,
// Accept styling props from the Server Component if needed
containerClassName,
buttonClassName,
// ... etc.
}) => {
// --- Define your functions here, INSIDE this Client Component ---
// Example optionsRender
const myClientOptionsRender = (item: YourOptionDataType) => {
return (
<div style={{ padding: '5px 10px' }}>
Option ID: **{item.id}** - {item.name}
</div>
);
};
// Example getOptionLabel
const myClientGetOptionLabel = (item: YourOptionDataType) => {
return `Selected: ${item.name}`;
};
// --- End of client-defined functions ---
return (
<Dropdown
options={options} // Pass serializable data
optionsRender={myClientOptionsRender} // Pass the client-defined function
getOptionLabel={myClientGetOptionLabel} // Pass the client-defined function
// Pass styling props down
containerClassName={containerClassName}
buttonClassName={buttonClassName}
// ... etc.
>
{children} {/* Pass the button text */}
</Dropdown>
);
};
export default ClientDropdownWrapper;
Example Usage in a Server Component (app/page.tsx
or app/layout.tsx
):
JavaScript
// app/page.tsx (This is a Server Component by default)
import React from 'react';
// Import the Client Wrapper, NOT the Dropdown directly if using function props
import ClientDropdownWrapper from '../components/ClientDropdownWrapper';
// Define types shared between server and client
interface YourOptionDataType {
id: string;
name: string;
// ... any other serializable properties fetched from the server
}
// Function to fetch serializable data (runs on the server)
async function fetchOptionsFromServer(): Promise<YourOptionDataType[]> {
// Example: Fetch data from an API or database
// Data fetched here MUST be serializable (JSON-compatible)
await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate delay
return [
{ id: 'a', name: 'Server Item One' },
{ id: 'b', name: 'Server Item Two' },
{ id: 'c', name: 'Server Item Three' },
];
}
export default async function ServerPage() {
const optionsData = await fetchOptionsFromServer(); // Data fetching happens on the server
return (
<div>
<h1>Welcome - Server Page</h1>
<p>This content is rendered on the server.</p>
{/* Render the Client Wrapper */}
{/* Pass serializable data and styling props (if needed) */}
<ClientDropdownWrapper
options={optionsData} // Pass the serializable data to the Client Wrapper
// Optional: Pass styling props from the Server Component
containerClassName="my-server-container-styles"
buttonClassName="my-server-button-styles"
>
{/* Children prop (button text) is serializable */}
Select a Server Option
</ClientDropdownWrapper>
<p>More server-rendered content...</p>
</div>
);
}
This pattern ensures that the functions required for client-side rendering are defined within the client boundary (ClientDropdownWrapper
), while still allowing you to fetch the initial data on the server and pass it down.
Prop Name | Type | Required | Description |
---|---|---|---|
children |
React.ReactNode |
Yes | The content (usually text or a simple element) displayed in the main dropdown button when no option is currently selected. |
options |
OptionType[] |
Yes | An array of the available options. OptionType can be string or any object type. |
optionSelector |
StringKeyPaths<OptionType> |
No |
(For Object Options) A dot-notation string path (e.g., "user.name" ) to the property in the option object to use as the display label. Only used if optionsRender is not provided. |
optionsRender |
(item: OptionType) => React.ReactNode |
No | (For Object Options) A function to render each option item in the dropdown list. Receives the option object and should return React JSX. Must be defined in a Client Component. |
getOptionLabel |
(item: OptionType) => string |
No |
(For Object Options) A function to get the string label displayed in the main dropdown button when an option is selected. Used if optionsRender is provided or if optionSelector is not sufficient. Must be defined in a Client Component.
|
containerClassName |
string |
No | CSS class name for the main div wrapper. |
containerStyle |
React.CSSProperties |
No | Inline CSS style object for the main div wrapper. |
buttonClassName |
string |
No | CSS class name for the main button element. |
buttonStyle |
React.CSSProperties |
No | Inline CSS style object for the main button element. |
listContainerClassName |
string |
No | CSS class name for the dropdown list div wrapper. |
listContainerStyle |
React.CSSProperties |
No | Inline CSS style object for the dropdown list div wrapper. |
listClassName |
string |
No | CSS class name for the ul element when optionsRender is not used. |
listStyle |
React.CSSProperties |
No | Inline CSS style object for the ul element when optionsRender is not used. |
listItemClassName |
string |
No | CSS class name for each li or button element within the dropdown list. |
onChange |
(value: OptionType) => void |
No | A callback function that is invoked whenever an option is selected. Receives the selected option (string or object ) as its argument. Must be defined in a Client Component.
|
listItemStyle |
React.CSSProperties |
No | Inline CSS style object for each li or button element within the dropdown list. |
Note: The OptionType
generic ensures type safety based on whether you provide string or object options and the related props (optionSelector
, optionsRender
, getOptionLabel
).
This package is written in TypeScript and includes type definitions. You should automatically get type hinting and checking when using the component in your TypeScript projects.
Pull requests are welcome! For major changes, please open an issue first to discuss what you would like to change.
This project uses Vitest for unit and integration testing. You can run the tests using the following pnpm
scripts:
-
Run all tests once:
pnpm test
-
Run tests in watch mode (re-runs on file changes):
pnpm test:watch
-
Open the Vitest UI for interactive testing:
pnpm test:ui
Please ensure all tests pass before submitting a pull request.