react-dropdown-light
TypeScript icon, indicating that this package has built-in type declarations

1.1.4 • Public • Published

react-dropdown-light

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.

Features

  • 🌍 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.
  • 🛡️ 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.

    Installation

npm

npm install react-dropdown-light

yarn

yarn add react-dropdown-light

pnpm

pnpm add react-dropdown-light

Basic Usage (String Options)

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;

Usage with Object Options

The component provides two main ways to handle object options: using a selector string or providing custom render/label functions.

Using optionSelector

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;

Using optionsRender and getOptionLabel (Advanced)

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;

Styling

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 main div wrapper (.relative).
  • buttonClassName, buttonStyle: For the main button element.
  • listContainerClassName, listContainerStyle: For the dropdown list div wrapper (.z-10 .absolute ...).
  • listClassName, listStyle: For the ul element when optionsRender is not used.
  • listItemClassName, listItemStyle: For each li or button 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.

Next.js App Router Compatibility

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:

  1. Create a Wrapper: Make a new component file (e.g., components/ClientDropdownWrapper.tsx). Add "use client"; at the very top.
  2. Define Functions in the Wrapper: Define your optionsRender and getOptionLabel functions inside this wrapper component.
  3. Pass Data from Server to Wrapper: Your Server Component will render the ClientDropdownWrapper and pass the necessary serializable data (like your options array) as props to the wrapper.
  4. Pass Data and Functions from Wrapper to Dropdown: The ClientDropdownWrapper will then render your Dropdown 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.

API Reference


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).

TypeScript Usage

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.

Contributing

Pull requests are welcome! For major changes, please open an issue first to discuss what you would like to change.

Running Tests

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.

Package Sidebar

Install

npm i react-dropdown-light

Weekly Downloads

44

Version

1.1.4

License

MIT

Unpacked Size

36.2 kB

Total Files

7

Last publish

Collaborators

  • m-grgic