A minimal, framework-agnostic PickList component for React 19 with isolated CSS and Formik-friendly events.
npm install react-picklist-liteA minimal, framework-agnostic PickList component for React 19 with isolated CSS and a Formik-friendly API. Works on Node.js 20–24.
- Unique CSS prefix: rpkl (no global leakage)
- Controlled component: pass source and target, receive updates via onChange
- TypeScript generics and ESM/CJS builds
- Formik/Yup friendly events
- Another UI library friendly (no design system dependency)
- Dark mode (auto via prefers-color-scheme or force via dark prop)
- Mobile responsive layout
- Built-in SVG icons for actions (move, move all, remove, reorder)
- Search filtering with clear and customizable matcher
- Reorder target list (top / up / down / bottom)
- Per-item disable hook to prevent moving
``bash`
npm install react-picklist-liteor
pnpm add react-picklist-lite
Import the bundled CSS once in your app:
`ts`
import 'react-picklist-lite/styles.css';
`tsx
import { useState } from 'react';
import { PickList } from 'react-picklist-lite';
import 'react-picklist-lite/styles.css';
type User = { id: number; name: string };
export default function Example() {
const [sourceUsers, setSourceUsers] = useState
{ id: 1, name: 'Ada' },
{ id: 2, name: 'Linus' },
{ id: 3, name: 'Grace' },
]);
const [targetUsers, setTargetUsers] = useState
return (
source={sourceUsers}
target={targetUsers}
onChange={(e) => {
setSourceUsers(e.source);
setTargetUsers(e.target);
}}
itemTemplate={(u) => {u.name}}
getKey={(u) => u.id}
sourceHeader="Available Users"
targetHeader="Assigned Users"
filterable
reorderable
showCounts
/>
);
}
`
`tsx
import { Formik, Form } from 'formik';
import * as Yup from 'yup';
import { PickList } from 'react-picklist-lite';
import 'react-picklist-lite/styles.css';
type User = { id: number; name: string };
const validationSchema = Yup.object({
users: Yup.array().of(
Yup.object({ id: Yup.number().required(), name: Yup.string().required() })
).min(1, 'Pick at least one user')
});
function FormikPickList({ allUsers }: { allUsers: User[] }) {
return (
initialValues={{ users: [] }}
validationSchema={validationSchema}
onSubmit={(values) => console.log(values)}
>
{({ values, setFieldValue, errors, touched }) => (
)}
);
}
`
`ts
export type PickListChangeEvent
source: T[];
target: T[];
originalEvent?: React.SyntheticEvent;
name?: string;
value?: T[]; // mirrors target for form libs
};
export type PickListProps
source: T[];
target: T[];
onChange?: (e: PickListChangeEvent
itemTemplate?: (item: T) => React.ReactNode;
sourceHeader?: React.ReactNode;
targetHeader?: React.ReactNode;
className?: string;
style?: React.CSSProperties;
name?: string; // forwarded in events
getKey?: (item: T) => string | number; // stable identity
sourceEmptyMessage?: React.ReactNode;
targetEmptyMessage?: React.ReactNode;
// Optional textual labels used as button titles/tooltips
moveAllLabel?: string;
moveSelectedLabel?: string;
removeSelectedLabel?: string;
removeAllLabel?: string;
disabled?: boolean;
// Advanced options
filterable?: boolean; // default true
sourceFilterPlaceholder?: string; // default 'Search...'
targetFilterPlaceholder?: string; // default 'Search...'
filterBy?: (item: T, text: string) => boolean; // custom matcher
reorderable?: boolean; // default true (reorder target)
showCounts?: boolean; // default true
dark?: boolean; // force dark theme
isItemDisabled?: (item: T) => boolean; // prevent selecting/moving
icons?: Partial<{
moveSelected: React.ReactNode;
moveAll: React.ReactNode;
removeSelected: React.ReactNode;
removeAll: React.ReactNode;
up: React.ReactNode; down: React.ReactNode; top: React.ReactNode; bottom: React.ReactNode;
clear: React.ReactNode; search: React.ReactNode;
}>;
ariaLabels?: Partial<{
moveSelected: string; moveAll: string; removeSelected: string; removeAll: string;
up: string; down: string; top: string; bottom: string;
clearFilter: string; sourceSearch: string; targetSearch: string;
}>;
};
`
- onChange receives { source, target, name, value }. value mirrors target to simplify integration with form libs.getKey
- Provide a stable for best performance and to avoid duplicates.dark
- Use prop to force dark mode or rely on system theme automatically.
- All selectors are scoped under the rpkl prefix. No global resets..rpkl
- You can theme via CSS vars on or a parent:
`css`
.rpkl { --rpkl-border: #cbd5e1; --rpkl-selected-bg: #e0e7ff; }
Dark mode variables are applied automatically when the OS prefers dark, or always when you add the rpkl--dark class (the component adds this if you pass dark={true}):
`tsx`...
`bash`
npm install
npm run build
- Dark mode theme (automatic and opt-in via dark prop)isItemDisabled` to lock items from moving/selecting
- Mobile responsive layout
- Search filtering with clear and customizable matcher
- SVG icons for all actions
- Reorder target items (top / up / down / bottom)
-