Skip to content

Instantly share code, notes, and snippets.

@ilkou
Created March 26, 2024 13:54
Show Gist options
  • Save ilkou/7bf2dbd42a7faf70053b43034fc4b5a4 to your computer and use it in GitHub Desktop.
Save ilkou/7bf2dbd42a7faf70053b43034fc4b5a4 to your computer and use it in GitHub Desktop.
react-select with shadcn/ui
/* ----------- simple-select.js ----------- */
import * as React from 'react';
import Select from 'react-select';
import type { Props } from 'react-select';
import { defaultClassNames, defaultStyles } from './helper';
import {
ClearIndicator,
DropdownIndicator,
MultiValueRemove,
Option
} from './components';
const SimpleSelect = React.forwardRef((props: Props, ref) => {
const {
value,
onChange,
options = [],
styles = defaultStyles,
classNames = defaultClassNames,
components = {},
...rest
} = props;
return (
<Select
ref={ref}
value={value}
onChange={onChange}
options={options}
unstyled
components={{
DropdownIndicator,
ClearIndicator,
MultiValueRemove,
Option,
...components
}}
styles={styles}
classNames={classNames}
{...rest}
/>
);
});
export default SimpleSelect;
/* ----------- helper.js ----------- */
import { cn } from 'lib/utils';
/**
* styles that aligns with shadcn/ui
*/
const controlStyles = {
base: 'flex !min-h-9 w-full rounded-md border border-input bg-transparent pl-3 py-1 pr-1 gap-1 text-sm shadow-sm transition-colors hover:cursor-pointer',
focus: 'outline-none ring-1 ring-ring',
disabled: 'cursor-not-allowed opacity-50'
};
const placeholderStyles = 'text-sm text-muted-foreground';
const valueContainerStyles = 'gap-1';
const multiValueStyles =
'inline-flex items-center gap-2 rounded-md border border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80 px-1.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2';
const indicatorsContainerStyles = 'gap-1';
const clearIndicatorStyles = 'p-1 rounded-md';
const indicatorSeparatorStyles = 'bg-border';
const dropdownIndicatorStyles = 'p-1 rounded-md';
const menuStyles =
'p-1 mt-1 border bg-popover shadow-md rounded-md text-popover-foreground';
const groupHeadingStyles =
'py-2 px-1 text-secondary-foreground text-sm font-semibold';
const optionStyles = {
base: 'hover:cursor-pointer hover:bg-accent hover:text-accent-foreground px-2 py-1.5 rounded-sm !text-sm !cursor-default !select-none !outline-none font-sans',
focus: 'active:bg-accent/90 bg-accent text-accent-foreground',
disabled: 'pointer-events-none opacity-50',
selected: ''
};
const noOptionsMessageStyles =
'text-accent-foreground p-2 bg-accent border border-dashed border-border rounded-sm';
const loadingIndicatorStyles =
'flex items-center justify-center h-4 w-4 opacity-50';
const loadingMessageStyles = 'text-accent-foreground p-2 bg-accent';
/**
* This factory method is used to build custom classNames configuration
*/
export const createClassNames = (classNames) => {
return {
clearIndicator: (state) =>
cn(clearIndicatorStyles, classNames?.clearIndicator?.(state)),
container: (state) => cn(classNames?.container?.(state)),
control: (state) =>
cn(
controlStyles.base,
state.isDisabled && controlStyles.disabled,
state.isFocused && controlStyles.focus,
classNames?.control?.(state)
),
dropdownIndicator: (state) =>
cn(dropdownIndicatorStyles, classNames?.dropdownIndicator?.(state)),
group: (state) => cn(classNames?.group?.(state)),
groupHeading: (state) =>
cn(groupHeadingStyles, classNames?.groupHeading?.(state)),
indicatorsContainer: (state) =>
cn(indicatorsContainerStyles, classNames?.indicatorsContainer?.(state)),
indicatorSeparator: (state) =>
cn(indicatorSeparatorStyles, classNames?.indicatorSeparator?.(state)),
input: (state) => cn(classNames?.input?.(state)),
loadingIndicator: (state) =>
cn(loadingIndicatorStyles, classNames?.loadingIndicator?.(state)),
loadingMessage: (state) =>
cn(loadingMessageStyles, classNames?.loadingMessage?.(state)),
menu: (state) => cn(menuStyles, classNames?.menu?.(state)),
menuList: (state) => cn(classNames?.menuList?.(state)),
menuPortal: (state) => cn(classNames?.menuPortal?.(state)),
multiValue: (state) =>
cn(multiValueStyles, classNames?.multiValue?.(state)),
multiValueLabel: (state) => cn(classNames?.multiValueLabel?.(state)),
multiValueRemove: (state) => cn(classNames?.multiValueRemove?.(state)),
noOptionsMessage: (state) =>
cn(noOptionsMessageStyles, classNames?.noOptionsMessage?.(state)),
option: (state) =>
cn(
optionStyles.base,
state.isFocused && optionStyles.focus,
state.isDisabled && optionStyles.disabled,
state.isSelected && optionStyles.selected,
classNames?.option?.(state)
),
placeholder: (state) =>
cn(placeholderStyles, classNames?.placeholder?.(state)),
singleValue: (state) => cn(classNames?.singleValue?.(state)),
valueContainer: (state) =>
cn(valueContainerStyles, classNames?.valueContainer?.(state))
};
};
export const defaultClassNames = createClassNames({});
export const defaultStyles = {
input: (base) => ({
...base,
'input:focus': {
boxShadow: 'none'
}
}),
multiValueLabel: (base) => ({
...base,
whiteSpace: 'normal',
overflow: 'visible'
}),
control: (base) => ({
...base,
transition: 'none'
// minHeight: '2.25rem', // we used !min-h-9 instead
}),
menuList: (base) => ({
...base,
'::-webkit-scrollbar': {
background: 'transparent'
},
'::-webkit-scrollbar-track': {
background: 'transparent'
},
'::-webkit-scrollbar-thumb': {
background: 'hsl(var(--border))'
},
'::-webkit-scrollbar-thumb:hover': {
background: 'transparent'
}
})
};
/* ----------- components.jsx ----------- */
import * as React from 'react';
import type {
ClearIndicatorProps,
DropdownIndicatorProps,
MultiValueRemoveProps,
OptionProps
} from 'react-select';
import { components } from 'react-select';
import {
CaretSortIcon,
CheckIcon,
Cross2Icon as CloseIcon
} from '@radix-ui/react-icons';
export const DropdownIndicator = (props: DropdownIndicatorProps) => {
return (
<components.DropdownIndicator {...props}>
<CaretSortIcon className={'h-4 w-4 opacity-50'} />
</components.DropdownIndicator>
);
};
export const ClearIndicator = (props: ClearIndicatorProps) => {
return (
<components.ClearIndicator {...props}>
<CloseIcon className={'h-3.5 w-3.5 opacity-50'} />
</components.ClearIndicator>
);
};
export const MultiValueRemove = (props: MultiValueRemoveProps) => {
return (
<components.MultiValueRemove {...props}>
<CloseIcon className={'h-3 w-3 opacity-50'} />
</components.MultiValueRemove>
);
};
export const Option = (props: OptionProps) => {
return (
<components.Option {...props}>
<div className="flex items-center justify-between">
<div>{props.data.label}</div>
{props.isSelected && <CheckIcon />}
</div>
</components.Option>
);
};
/* ----------- async-select.jsx ----------- */
import * as React from 'react';
import Async from 'react-select/async';
import type { Props } from 'react-select/async';
import { defaultClassNames, defaultStyles } from './helper';
import {
ClearIndicator,
DropdownIndicator,
MultiValueRemove,
Option
} from './components';
const AsyncSelect = React.forwardRef((props: Props, ref) => {
const {
value,
onChange,
styles = defaultStyles,
classNames = defaultClassNames,
components = {},
...rest
} = props;
return (
<Async
ref={ref}
value={value}
onChange={onChange}
unstyled
components={{
DropdownIndicator,
ClearIndicator,
MultiValueRemove,
Option,
...components
}}
styles={styles}
classNames={classNames}
{...rest}
/>
);
});
export default AsyncSelect;
/* ----------- hooks.jsx ----------- */
/**
* This hook could be added to your select component if needed:
* const formatters = useFormatters()
* <Select
* // other props
* {...formatters}
* />
*/
export const useFormatters = () => {
// useful for CreatableSelect
const formatCreateLabel = (label) => (
<span className={'text-sm'}>
Add
<span className={'font-semibold'}>{` "${label}"`}</span>
</span>
);
// useful for GroupedOptions
const formatGroupLabel = (data) => (
<div className={'flex justify-between items-center'}>
<span>{data.label}</span>
<span
className={
'rounded-md text-xs font-normal text-secondary-foreground bg-secondary shadow-sm px-1'
}
>
{data.options.length}
</span>
</div>
);
return {
formatCreateLabel,
formatGroupLabel
};
};
@roo12312
Copy link

how to use it? any example?

@ilkou
Copy link
Author

ilkou commented Mar 27, 2024

how to use it? any example?

The usage is similar to any react-select component.
For example, the AsyncSelect component provides custom styling options and allows further customization, but its usage is exactly the same as the Async component in react-select:

import React from 'react';

import AsyncSelect from 'components/ui/async-select'; // instead of 'react-select/async'
import { ColourOption, colourOptions } from '../data';

const filterColors = (inputValue: string) => {
  return colourOptions.filter((i) =>
    i.label.toLowerCase().includes(inputValue.toLowerCase())
  );
};

const loadOptions = (
  inputValue: string,
  callback: (options: ColourOption[]) => void
) => {
  setTimeout(() => {
    callback(filterColors(inputValue));
  }, 1000);
};

export default () => (
  <AsyncSelect cacheOptions loadOptions={loadOptions} defaultOptions />
);

@roo12312
Copy link

thanks.. it would be nice if you could add typescript

@ilkou
Copy link
Author

ilkou commented Mar 27, 2024

thanks.. it would be nice if you could add typescript

the types are also inferred from react-select:

import type { Props } from 'react-select';
// import type { Props } from 'react-select/async'; // for AsyncSelect

and it's working in my project (I use flow)

Screenshot 2024-03-27 at 12 13 28

but if it didn't work for u in typescript u can refer to react-select with typescript

@ilkou
Copy link
Author

ilkou commented Apr 22, 2024

I recently migrated from flow to TypeScript and I'm using the following:

const CreatableSelect = React.forwardRef<
  React.ElementRef<typeof Creatable>,
  React.ComponentPropsWithoutRef<typeof Creatable>
>((props, ref) => {

@H7ioo
Copy link

H7ioo commented Jun 22, 2024

For anyone who is looking for a complete typed code.
Note: I added React Window
I would also like to thank the creator for publishing such a useful code 🙏

// ~~~ helper.ts
// SOURCE: https://gist.github.com/ilkou/7bf2dbd42a7faf70053b43034fc4b5a4
import { cn } from "./utils";
import {
  type ClassNamesConfig,
  type GroupBase,
  type StylesConfig,
} from "react-select";
/**
 * styles that aligns with shadcn/ui
 */
const controlStyles = {
  base: "flex !min-h-9 w-full rounded-md border border-input bg-transparent pl-3 py-1 pr-1 gap-1 text-sm shadow-sm transition-colors hover:cursor-pointer",
  focus: "outline-none ring-1 ring-ring",
  disabled: "cursor-not-allowed opacity-50",
};
const placeholderStyles = "text-sm text-muted-foreground";
const valueContainerStyles = "gap-1";
const multiValueStyles =
  "inline-flex items-center gap-2 rounded-md border border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80 px-1.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2";
const indicatorsContainerStyles = "gap-1";
const clearIndicatorStyles = "p-1 rounded-md";
const indicatorSeparatorStyles = "bg-border";
const dropdownIndicatorStyles = "p-1 rounded-md";
const menuStyles =
  "p-1 mt-1 border bg-popover shadow-md rounded-md text-popover-foreground";
const groupHeadingStyles =
  "py-2 px-1 text-secondary-foreground text-sm font-semibold";
const optionStyles = {
  base: "hover:cursor-pointer hover:bg-accent hover:text-accent-foreground px-2 py-1.5 rounded-sm !text-sm !cursor-default !select-none !outline-none font-sans",
  focus: "active:bg-accent/90 bg-accent text-accent-foreground",
  disabled: "pointer-events-none opacity-50",
  selected: "",
};
const noOptionsMessageStyles =
  "text-accent-foreground p-2 bg-accent border border-dashed border-border rounded-sm";
const loadingIndicatorStyles =
  "flex items-center justify-center h-4 w-4 opacity-50";
const loadingMessageStyles = "text-accent-foreground p-2 bg-accent";

/**
 * This factory method is used to build custom classNames configuration
 */
export const createClassNames = (
  classNames: ClassNamesConfig,
): ClassNamesConfig => {
  return {
    clearIndicator: (state) =>
      cn(clearIndicatorStyles, classNames?.clearIndicator?.(state)),
    container: (state) => cn(classNames?.container?.(state)),
    control: (state) =>
      cn(
        controlStyles.base,
        state.isDisabled && controlStyles.disabled,
        state.isFocused && controlStyles.focus,
        classNames?.control?.(state),
      ),
    dropdownIndicator: (state) =>
      cn(dropdownIndicatorStyles, classNames?.dropdownIndicator?.(state)),
    group: (state) => cn(classNames?.group?.(state)),
    groupHeading: (state) =>
      cn(groupHeadingStyles, classNames?.groupHeading?.(state)),
    indicatorsContainer: (state) =>
      cn(indicatorsContainerStyles, classNames?.indicatorsContainer?.(state)),
    indicatorSeparator: (state) =>
      cn(indicatorSeparatorStyles, classNames?.indicatorSeparator?.(state)),
    input: (state) => cn(classNames?.input?.(state)),
    loadingIndicator: (state) =>
      cn(loadingIndicatorStyles, classNames?.loadingIndicator?.(state)),
    loadingMessage: (state) =>
      cn(loadingMessageStyles, classNames?.loadingMessage?.(state)),
    menu: (state) => cn(menuStyles, classNames?.menu?.(state)),
    menuList: (state) => cn(classNames?.menuList?.(state)),
    menuPortal: (state) => cn(classNames?.menuPortal?.(state)),
    multiValue: (state) =>
      cn(multiValueStyles, classNames?.multiValue?.(state)),
    multiValueLabel: (state) => cn(classNames?.multiValueLabel?.(state)),
    multiValueRemove: (state) => cn(classNames?.multiValueRemove?.(state)),
    noOptionsMessage: (state) =>
      cn(noOptionsMessageStyles, classNames?.noOptionsMessage?.(state)),
    option: (state) =>
      cn(
        optionStyles.base,
        state.isFocused && optionStyles.focus,
        state.isDisabled && optionStyles.disabled,
        state.isSelected && optionStyles.selected,
        classNames?.option?.(state),
      ),
    placeholder: (state) =>
      cn(placeholderStyles, classNames?.placeholder?.(state)),
    singleValue: (state) => cn(classNames?.singleValue?.(state)),
    valueContainer: (state) =>
      cn(valueContainerStyles, classNames?.valueContainer?.(state)),
  };
};
export const defaultClassNames = createClassNames({});
export const defaultStyles: StylesConfig<
  unknown,
  boolean,
  GroupBase<unknown>
> = {
  input: (base) => ({
    ...base,
    "input:focus": {
      boxShadow: "none",
    },
  }),
  multiValueLabel: (base) => ({
    ...base,
    whiteSpace: "normal",
    overflow: "visible",
  }),
  control: (base) => ({
    ...base,
    transition: "none",
    // minHeight: '2.25rem', // we used !min-h-9 instead
  }),
  menuList: (base) => ({
    ...base,
    "::-webkit-scrollbar": {
      background: "transparent",
    },
    "::-webkit-scrollbar-track": {
      background: "transparent",
    },
    "::-webkit-scrollbar-thumb": {
      background: "hsl(var(--border))",
    },
    "::-webkit-scrollbar-thumb:hover": {
      background: "transparent",
    },
  }),
};

// ~~~ ReactSelectCustomComponents.tsx
// SOURCE: https://gist.github.com/ilkou/7bf2dbd42a7faf70053b43034fc4b5a4
import * as React from "react";
import type {
  ClearIndicatorProps,
  DropdownIndicatorProps,
  MenuListProps,
  MenuProps,
  MultiValueRemoveProps,
  OptionProps,
} from "react-select";
import { components } from "react-select";
import {
  CaretSortIcon,
  CheckIcon,
  Cross2Icon as CloseIcon,
} from "@radix-ui/react-icons";
import { FixedSizeList as List } from "react-window";

export const DropdownIndicator = (props: DropdownIndicatorProps) => {
  return (
    <components.DropdownIndicator {...props}>
      <CaretSortIcon className={"h-4 w-4 opacity-50"} />
    </components.DropdownIndicator>
  );
};

export const ClearIndicator = (props: ClearIndicatorProps) => {
  return (
    <components.ClearIndicator {...props}>
      <CloseIcon className={"h-3.5 w-3.5 opacity-50"} />
    </components.ClearIndicator>
  );
};

export const MultiValueRemove = (props: MultiValueRemoveProps) => {
  return (
    <components.MultiValueRemove {...props}>
      <CloseIcon className={"h-3 w-3 opacity-50"} />
    </components.MultiValueRemove>
  );
};

export const Option = (props: OptionProps) => {
  return (
    <components.Option {...props}>
      <div className="flex items-center justify-between">
        {/* TODO: Figure out the type */}
        <div>{(props.data as { label: string }).label}</div>
        {props.isSelected && <CheckIcon />}
      </div>
    </components.Option>
  );
};

// Using Menu and MenuList fixes the scrolling behavior
export const Menu = (props: MenuProps) => {
  return <components.Menu {...props}>{props.children}</components.Menu>;
};

export const MenuList = (props: MenuListProps) => {
  const { children, maxHeight } = props;

  const childrenArray = React.Children.toArray(children);

  const calculateHeight = () => {
    // When using children it resizes correctly
    const totalHeight = childrenArray.length * 35; // Adjust item height if different
    return totalHeight < maxHeight ? totalHeight : maxHeight;
  };

  const height = calculateHeight();

  // Ensure childrenArray has length. Even when childrenArray is empty there is one element left
  if (!childrenArray || childrenArray.length - 1 === 0) {
    return <components.MenuList {...props} />;
  }
  return (
    <List
      height={height}
      itemCount={childrenArray.length}
      itemSize={35} // Adjust item height if different
      width="100%"
    >
      {({ index, style }) => <div style={style}>{childrenArray[index]}</div>}
    </List>
  );
};
// ~~~ Select.tsx
// SOURCE: https://gist.github.com/ilkou/7bf2dbd42a7faf70053b43034fc4b5a4
import * as React from "react";
import SelectComponent from "react-select";
import type { Props } from "react-select";
import { defaultClassNames, defaultStyles } from "~/lib/helper";
import {
  ClearIndicator,
  DropdownIndicator,
  MultiValueRemove,
  Option,
  Menu,
  MenuList,
} from "./ReactSelectCustomComponents";

const Select = React.forwardRef<
  React.ElementRef<typeof SelectComponent>,
  React.ComponentPropsWithoutRef<typeof SelectComponent>
>((props: Props, ref) => {
  const {
    value,
    onChange,
    options = [],
    styles = defaultStyles,
    classNames = defaultClassNames,
    components = {},
    ...rest
  } = props;

  const id = React.useId();

  return (
    <SelectComponent
      instanceId={id}
      ref={ref}
      value={value}
      onChange={onChange}
      options={options}
      unstyled
      components={{
        DropdownIndicator,
        ClearIndicator,
        MultiValueRemove,
        Option,
        Menu,
        MenuList,
        ...components,
      }}
      styles={styles}
      classNames={classNames}
      {...rest}
    />
  );
});
Select.displayName = "Select";
export default Select;

// ~~~ Creatable.tsx
// SOURCE: https://gist.github.com/ilkou/7bf2dbd42a7faf70053b43034fc4b5a4
import * as React from "react";
import CreatableSelect from "react-select/creatable";
import type { Props } from "react-select";
import { defaultClassNames, defaultStyles } from "~/lib/helper";
import {
  ClearIndicator,
  DropdownIndicator,
  Menu,
  MenuList,
  MultiValueRemove,
  Option,
} from "./ReactSelectCustomComponents";

const Creatable = React.forwardRef<
  React.ElementRef<typeof CreatableSelect>,
  React.ComponentPropsWithoutRef<typeof CreatableSelect>
>((props: Props, ref) => {
  const {
    value,
    onChange,
    options = [],
    styles = defaultStyles,
    classNames = defaultClassNames,
    components = {},
    ...rest
  } = props;

  const id = React.useId();

  return (
    <CreatableSelect
      instanceId={id}
      ref={ref}
      value={value}
      onChange={onChange}
      options={options}
      unstyled
      components={{
        DropdownIndicator,
        ClearIndicator,
        MultiValueRemove,
        Option,
        Menu,
        MenuList,
        ...components,
      }}
      styles={styles}
      classNames={classNames}
      {...rest}
    />
  );
});
Creatable.displayName = "Creatable";
export default Creatable;

// ~~~ useFormatters.tsx
// SOURCE: https://gist.github.com/ilkou/7bf2dbd42a7faf70053b43034fc4b5a4

import { type CreatableAdditionalProps } from "node_modules/react-select/dist/declarations/src/useCreatable";
import { type GroupBase } from "react-select";

/**
 * This hook could be added to your select component if needed:
 *   const formatters = useFormatters()
 *   <Select
 *     // other props
 *     {...formatters}
 *   />
 */
export const useFormatters = () => {
  // useful for CreatableSelect
  const formatCreateLabel: CreatableAdditionalProps<
    unknown,
    GroupBase<unknown>
  >["formatCreateLabel"] = (label) => (
    <span className={"text-sm"}>
      Add
      <span className={"font-semibold"}>{` "${label}"`}</span>
    </span>
  );

  // useful for GroupedOptions
  const formatGroupLabel: (group: GroupBase<unknown>) => React.ReactNode = (
    data,
  ) => (
    <div className={"flex items-center justify-between"}>
      <span>{data.label}</span>
      <span
        className={
          "rounded-md bg-secondary px-1 text-xs font-normal text-secondary-foreground shadow-sm"
        }
      >
        {data.options.length}
      </span>
    </div>
  );
  return {
    formatCreateLabel,
    formatGroupLabel,
  };
};

@feliche93
Copy link

For anyone who is looking for a complete typed code. Note: I added React Window I would also like to thank the creator for publishing such a useful code 🙏

// ~~~ helper.ts
// SOURCE: https://gist.github.com/ilkou/7bf2dbd42a7faf70053b43034fc4b5a4
import { cn } from "./utils";
import {
  type ClassNamesConfig,
  type GroupBase,
  type StylesConfig,
} from "react-select";
/**
 * styles that aligns with shadcn/ui
 */
const controlStyles = {
  base: "flex !min-h-9 w-full rounded-md border border-input bg-transparent pl-3 py-1 pr-1 gap-1 text-sm shadow-sm transition-colors hover:cursor-pointer",
  focus: "outline-none ring-1 ring-ring",
  disabled: "cursor-not-allowed opacity-50",
};
const placeholderStyles = "text-sm text-muted-foreground";
const valueContainerStyles = "gap-1";
const multiValueStyles =
  "inline-flex items-center gap-2 rounded-md border border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80 px-1.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2";
const indicatorsContainerStyles = "gap-1";
const clearIndicatorStyles = "p-1 rounded-md";
const indicatorSeparatorStyles = "bg-border";
const dropdownIndicatorStyles = "p-1 rounded-md";
const menuStyles =
  "p-1 mt-1 border bg-popover shadow-md rounded-md text-popover-foreground";
const groupHeadingStyles =
  "py-2 px-1 text-secondary-foreground text-sm font-semibold";
const optionStyles = {
  base: "hover:cursor-pointer hover:bg-accent hover:text-accent-foreground px-2 py-1.5 rounded-sm !text-sm !cursor-default !select-none !outline-none font-sans",
  focus: "active:bg-accent/90 bg-accent text-accent-foreground",
  disabled: "pointer-events-none opacity-50",
  selected: "",
};
const noOptionsMessageStyles =
  "text-accent-foreground p-2 bg-accent border border-dashed border-border rounded-sm";
const loadingIndicatorStyles =
  "flex items-center justify-center h-4 w-4 opacity-50";
const loadingMessageStyles = "text-accent-foreground p-2 bg-accent";

/**
 * This factory method is used to build custom classNames configuration
 */
export const createClassNames = (
  classNames: ClassNamesConfig,
): ClassNamesConfig => {
  return {
    clearIndicator: (state) =>
      cn(clearIndicatorStyles, classNames?.clearIndicator?.(state)),
    container: (state) => cn(classNames?.container?.(state)),
    control: (state) =>
      cn(
        controlStyles.base,
        state.isDisabled && controlStyles.disabled,
        state.isFocused && controlStyles.focus,
        classNames?.control?.(state),
      ),
    dropdownIndicator: (state) =>
      cn(dropdownIndicatorStyles, classNames?.dropdownIndicator?.(state)),
    group: (state) => cn(classNames?.group?.(state)),
    groupHeading: (state) =>
      cn(groupHeadingStyles, classNames?.groupHeading?.(state)),
    indicatorsContainer: (state) =>
      cn(indicatorsContainerStyles, classNames?.indicatorsContainer?.(state)),
    indicatorSeparator: (state) =>
      cn(indicatorSeparatorStyles, classNames?.indicatorSeparator?.(state)),
    input: (state) => cn(classNames?.input?.(state)),
    loadingIndicator: (state) =>
      cn(loadingIndicatorStyles, classNames?.loadingIndicator?.(state)),
    loadingMessage: (state) =>
      cn(loadingMessageStyles, classNames?.loadingMessage?.(state)),
    menu: (state) => cn(menuStyles, classNames?.menu?.(state)),
    menuList: (state) => cn(classNames?.menuList?.(state)),
    menuPortal: (state) => cn(classNames?.menuPortal?.(state)),
    multiValue: (state) =>
      cn(multiValueStyles, classNames?.multiValue?.(state)),
    multiValueLabel: (state) => cn(classNames?.multiValueLabel?.(state)),
    multiValueRemove: (state) => cn(classNames?.multiValueRemove?.(state)),
    noOptionsMessage: (state) =>
      cn(noOptionsMessageStyles, classNames?.noOptionsMessage?.(state)),
    option: (state) =>
      cn(
        optionStyles.base,
        state.isFocused && optionStyles.focus,
        state.isDisabled && optionStyles.disabled,
        state.isSelected && optionStyles.selected,
        classNames?.option?.(state),
      ),
    placeholder: (state) =>
      cn(placeholderStyles, classNames?.placeholder?.(state)),
    singleValue: (state) => cn(classNames?.singleValue?.(state)),
    valueContainer: (state) =>
      cn(valueContainerStyles, classNames?.valueContainer?.(state)),
  };
};
export const defaultClassNames = createClassNames({});
export const defaultStyles: StylesConfig<
  unknown,
  boolean,
  GroupBase<unknown>
> = {
  input: (base) => ({
    ...base,
    "input:focus": {
      boxShadow: "none",
    },
  }),
  multiValueLabel: (base) => ({
    ...base,
    whiteSpace: "normal",
    overflow: "visible",
  }),
  control: (base) => ({
    ...base,
    transition: "none",
    // minHeight: '2.25rem', // we used !min-h-9 instead
  }),
  menuList: (base) => ({
    ...base,
    "::-webkit-scrollbar": {
      background: "transparent",
    },
    "::-webkit-scrollbar-track": {
      background: "transparent",
    },
    "::-webkit-scrollbar-thumb": {
      background: "hsl(var(--border))",
    },
    "::-webkit-scrollbar-thumb:hover": {
      background: "transparent",
    },
  }),
};

// ~~~ ReactSelectCustomComponents.tsx
// SOURCE: https://gist.github.com/ilkou/7bf2dbd42a7faf70053b43034fc4b5a4
import * as React from "react";
import type {
  ClearIndicatorProps,
  DropdownIndicatorProps,
  MenuListProps,
  MenuProps,
  MultiValueRemoveProps,
  OptionProps,
} from "react-select";
import { components } from "react-select";
import {
  CaretSortIcon,
  CheckIcon,
  Cross2Icon as CloseIcon,
} from "@radix-ui/react-icons";
import { FixedSizeList as List } from "react-window";

export const DropdownIndicator = (props: DropdownIndicatorProps) => {
  return (
    <components.DropdownIndicator {...props}>
      <CaretSortIcon className={"h-4 w-4 opacity-50"} />
    </components.DropdownIndicator>
  );
};

export const ClearIndicator = (props: ClearIndicatorProps) => {
  return (
    <components.ClearIndicator {...props}>
      <CloseIcon className={"h-3.5 w-3.5 opacity-50"} />
    </components.ClearIndicator>
  );
};

export const MultiValueRemove = (props: MultiValueRemoveProps) => {
  return (
    <components.MultiValueRemove {...props}>
      <CloseIcon className={"h-3 w-3 opacity-50"} />
    </components.MultiValueRemove>
  );
};

export const Option = (props: OptionProps) => {
  return (
    <components.Option {...props}>
      <div className="flex items-center justify-between">
        {/* TODO: Figure out the type */}
        <div>{(props.data as { label: string }).label}</div>
        {props.isSelected && <CheckIcon />}
      </div>
    </components.Option>
  );
};

// Using Menu and MenuList fixes the scrolling behavior
export const Menu = (props: MenuProps) => {
  return <components.Menu {...props}>{props.children}</components.Menu>;
};

export const MenuList = (props: MenuListProps) => {
  const { children, maxHeight } = props;

  const childrenArray = React.Children.toArray(children);

  const calculateHeight = () => {
    // When using children it resizes correctly
    const totalHeight = childrenArray.length * 35; // Adjust item height if different
    return totalHeight < maxHeight ? totalHeight : maxHeight;
  };

  const height = calculateHeight();

  // Ensure childrenArray has length. Even when childrenArray is empty there is one element left
  if (!childrenArray || childrenArray.length - 1 === 0) {
    return <components.MenuList {...props} />;
  }
  return (
    <List
      height={height}
      itemCount={childrenArray.length}
      itemSize={35} // Adjust item height if different
      width="100%"
    >
      {({ index, style }) => <div style={style}>{childrenArray[index]}</div>}
    </List>
  );
};
// ~~~ Select.tsx
// SOURCE: https://gist.github.com/ilkou/7bf2dbd42a7faf70053b43034fc4b5a4
import * as React from "react";
import SelectComponent from "react-select";
import type { Props } from "react-select";
import { defaultClassNames, defaultStyles } from "~/lib/helper";
import {
  ClearIndicator,
  DropdownIndicator,
  MultiValueRemove,
  Option,
  Menu,
  MenuList,
} from "./ReactSelectCustomComponents";

const Select = React.forwardRef<
  React.ElementRef<typeof SelectComponent>,
  React.ComponentPropsWithoutRef<typeof SelectComponent>
>((props: Props, ref) => {
  const {
    value,
    onChange,
    options = [],
    styles = defaultStyles,
    classNames = defaultClassNames,
    components = {},
    ...rest
  } = props;

  const id = React.useId();

  return (
    <SelectComponent
      instanceId={id}
      ref={ref}
      value={value}
      onChange={onChange}
      options={options}
      unstyled
      components={{
        DropdownIndicator,
        ClearIndicator,
        MultiValueRemove,
        Option,
        Menu,
        MenuList,
        ...components,
      }}
      styles={styles}
      classNames={classNames}
      {...rest}
    />
  );
});
Select.displayName = "Select";
export default Select;

// ~~~ Creatable.tsx
// SOURCE: https://gist.github.com/ilkou/7bf2dbd42a7faf70053b43034fc4b5a4
import * as React from "react";
import CreatableSelect from "react-select/creatable";
import type { Props } from "react-select";
import { defaultClassNames, defaultStyles } from "~/lib/helper";
import {
  ClearIndicator,
  DropdownIndicator,
  Menu,
  MenuList,
  MultiValueRemove,
  Option,
} from "./ReactSelectCustomComponents";

const Creatable = React.forwardRef<
  React.ElementRef<typeof CreatableSelect>,
  React.ComponentPropsWithoutRef<typeof CreatableSelect>
>((props: Props, ref) => {
  const {
    value,
    onChange,
    options = [],
    styles = defaultStyles,
    classNames = defaultClassNames,
    components = {},
    ...rest
  } = props;

  const id = React.useId();

  return (
    <CreatableSelect
      instanceId={id}
      ref={ref}
      value={value}
      onChange={onChange}
      options={options}
      unstyled
      components={{
        DropdownIndicator,
        ClearIndicator,
        MultiValueRemove,
        Option,
        Menu,
        MenuList,
        ...components,
      }}
      styles={styles}
      classNames={classNames}
      {...rest}
    />
  );
});
Creatable.displayName = "Creatable";
export default Creatable;

// ~~~ useFormatters.tsx
// SOURCE: https://gist.github.com/ilkou/7bf2dbd42a7faf70053b43034fc4b5a4

import { type CreatableAdditionalProps } from "node_modules/react-select/dist/declarations/src/useCreatable";
import { type GroupBase } from "react-select";

/**
 * This hook could be added to your select component if needed:
 *   const formatters = useFormatters()
 *   <Select
 *     // other props
 *     {...formatters}
 *   />
 */
export const useFormatters = () => {
  // useful for CreatableSelect
  const formatCreateLabel: CreatableAdditionalProps<
    unknown,
    GroupBase<unknown>
  >["formatCreateLabel"] = (label) => (
    <span className={"text-sm"}>
      Add
      <span className={"font-semibold"}>{` "${label}"`}</span>
    </span>
  );

  // useful for GroupedOptions
  const formatGroupLabel: (group: GroupBase<unknown>) => React.ReactNode = (
    data,
  ) => (
    <div className={"flex items-center justify-between"}>
      <span>{data.label}</span>
      <span
        className={
          "rounded-md bg-secondary px-1 text-xs font-normal text-secondary-foreground shadow-sm"
        }
      >
        {data.options.length}
      </span>
    </div>
  );
  return {
    formatCreateLabel,
    formatGroupLabel,
  };
};

@H7ioo this is super helpful 🙏 Do you also have the async-select component typed by any chance? I get an error for the Props attribute. Thanks for sharing this with the community

@H7ioo
Copy link

H7ioo commented Aug 4, 2024

@feliche93.
It is actually the same. You have to update the import only.

// SOURCE: https://gist.github.com/ilkou/7bf2dbd42a7faf70053b43034fc4b5a4
import * as React from "react";
import AsyncSelectComponent from "react-select/async";
import type { Props } from "react-select";
import { defaultClassNames, defaultStyles } from "~/lib/helper";
import {
  ClearIndicator,
  DropdownIndicator,
  MultiValueRemove,
  Option,
  Menu,
  MenuList,
} from "./ReactSelectCustomComponents";

const AsyncSelect = React.forwardRef<
  React.ElementRef<typeof AsyncSelectComponent>,
  React.ComponentPropsWithoutRef<typeof AsyncSelectComponent>
>((props: Props, ref) => {
  const {
    value,
    onChange,
    options = [],
    styles = defaultStyles,
    classNames = defaultClassNames,
    components = {},
    ...rest
  } = props;

  const id = React.useId();

  return (
    <AsyncSelectComponent
      className="HELLO"
      instanceId={id}
      ref={ref}
      value={value}
      onChange={onChange}
      options={options}
      unstyled
      components={{
        DropdownIndicator,
        ClearIndicator,
        MultiValueRemove,
        Option,
        Menu,
        MenuList,
        ...components,
      }}
      styles={styles}
      classNames={classNames}
      {...rest}
    />
  );
});
AsyncSelect.displayName = "Async Select";
export default AsyncSelect;

@fitimbytyqi
Copy link

Thanks a lot for this code snippet. however I think that there is an issue with typescript here.

`const options: { value: string; label: string }[] = [
{ value: "chocolate", label: "Chocolate" },
{ value: "strawberry", label: "Strawberry" },
{ value: "vanilla", label: "Vanilla" },
];

<Select
options={options}
onChange={(value) => console.log(value)}
/>`

In this case value's type is unknown and not the one from options. Any idea how we can solve this ?

@H7ioo
Copy link

H7ioo commented Aug 28, 2024

You're welcome.
If you import Select from "react-select" you would have the same type.
The issue is from react-select itself.

I found a way to resolve the issue but you have to update all the components.
Select component receives a generic Option which is what you are looking for.

    /** The value of the select; reflected by the selected option */
    value: PropsValue<Option>;

To make this work you should update the Select component to receive a generic "OptionType" which set for default to {label: string, value: string}.
Then you need to update the SelectComponent generic to have that OptionType like this SelectComponent<OptionType>

const Select = React.forwardRef<
  React.ElementRef<typeof SelectComponent<OptionType>>,
  React.ComponentPropsWithoutRef<typeof SelectComponent<OptionType>>
>((props: Props<OptionType>, ref) => {
  const {
    value, // This is OptionType
    onChange,
    options = [],
    styles = defaultStyles,
    classNames = defaultClassNames,
    components = {},
    ...rest
  } = props;

  const id = React.useId();

  return (
    <SelectComponent
      instanceId={id}
      ref={ref}
      value={value} // Now this is OptionType
      onChange={onChange}
      options={options}
      unstyled
      components={{
        DropdownIndicator,
        ClearIndicator,
        MultiValueRemove,
        Option,
        Menu,
        MenuList,
        ...components,
      }}
      styles={styles}
      classNames={classNames}
      {...rest}
    />
  );
});

This will cause errors because the other components don't have the same Option type

<SelectComponent
...
components={{
DropdownIndicator, // Those components need to be updated to have the OptionType generic
...
}}
...
/>

☝️ Don't forget you have to wrap the forwardRef component so you can receive generic.
Those solutions could help: https://stackoverflow.com/questions/58469229/react-with-typescript-generics-while-using-react-forwardref

export const DropdownIndicator = <OptionType extends object = {label:string, value: string}>(props: DropdownIndicatorProps<OptionType>) => {
  return (
    <components.DropdownIndicator {...props}>
      <CaretSortIcon className={"h-4 w-4 opacity-50"} />
    </components.DropdownIndicator>
  );
};

And so on to resolve all the errors.

I hope this was clear and helpful.

@fitimbytyqi
Copy link

@H7ioo Hey, thanks a lot for the feedback, I had found a solution similar but yours looks more clean, I'll try it out soon.

@fitimbytyqi
Copy link

fitimbytyqi commented Sep 17, 2024

For anyone trying to make this component super generic with typescript, here's what I came up with the help of @H7ioo, thanks a lot

import React, { ReactElement, Ref } from 'react';
import SelectComponent, {
  components,
  ClassNamesConfig,
  DropdownIndicatorProps,
  GroupBase,
  StylesConfig,
  MultiValueRemoveProps,
  ClearIndicatorProps,
  OptionProps,
  MenuProps,
  MenuListProps,
  Props,
  SelectInstance,
  createFilter,
} from 'react-select';
import { FixedSizeList as List } from 'react-window';
import { cn } from '@/lib/utils';
import { Check, ChevronDown, X } from 'lucide-react';

/** select option type */
export type OptionType = { label: string; value: string | number };

/**
 * styles that aligns with shadcn/ui
 */
const selectStyles = {
  controlStyles: {
    base: 'flex !min-h-9 w-full rounded-md border border-input bg-transparent pl-3 py-1 pr-1 gap-1 text-sm shadow-sm transition-colors hover:cursor-pointer',
    focus: 'outline-none ring-1 ring-ring',
    disabled: 'cursor-not-allowed opacity-50',
  },
  placeholderStyles: 'text-muted-foreground text-sm ml-1 font-medium',
  valueContainerStyles: 'gap-1',
  multiValueStyles:
    'inline-flex items-center gap-2 rounded-md border border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80 px-1.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
  indicatorsContainerStyles: 'gap-1',
  clearIndicatorStyles: 'p-1 rounded-md',
  indicatorSeparatorStyles: 'bg-muted',
  dropdownIndicatorStyles: 'p-1 rounded-md',
  menu: 'mt-1.5 p-1.5 border border-input bg-background text-sm rounded-lg',
  menuList: 'morel-scrollbar',
  groupHeadingStyles:
    'py-2 px-1 text-secondary-foreground text-sm font-semibold',
  optionStyles: {
    base: 'hover:cursor-pointer hover:bg-accent hover:text-accent-foreground px-2 py-1.5 rounded-sm !text-sm !cursor-default !select-none !outline-none font-sans',
    focus: 'active:bg-accent/90 bg-accent text-accent-foreground',
    disabled: 'pointer-events-none opacity-50',
    selected: '',
  },
  noOptionsMessageStyles:
    'text-muted-foreground py-4 text-center text-sm border border-border rounded-sm',
  label: 'text-muted-foreground text-sm',
  loadingIndicatorStyles: 'flex items-center justify-center h-4 w-4 opacity-50',
  loadingMessageStyles: 'text-accent-foreground p-2 bg-accent',
};

/**
 * This factory method is used to build custom classNames configuration
 */
export const createClassNames = (
  classNames: ClassNamesConfig<OptionType, boolean, GroupBase<OptionType>>
): ClassNamesConfig<OptionType, boolean, GroupBase<OptionType>> => {
  return {
    clearIndicator: (state) =>
      cn(
        selectStyles.clearIndicatorStyles,
        classNames?.clearIndicator?.(state)
      ),
    container: (state) => cn(classNames?.container?.(state)),
    control: (state) =>
      cn(
        selectStyles.controlStyles.base,
        state.isDisabled && selectStyles.controlStyles.disabled,
        state.isFocused && selectStyles.controlStyles.focus,
        classNames?.control?.(state)
      ),
    dropdownIndicator: (state) =>
      cn(
        selectStyles.dropdownIndicatorStyles,
        classNames?.dropdownIndicator?.(state)
      ),
    group: (state) => cn(classNames?.group?.(state)),
    groupHeading: (state) =>
      cn(selectStyles.groupHeadingStyles, classNames?.groupHeading?.(state)),
    indicatorsContainer: (state) =>
      cn(
        selectStyles.indicatorsContainerStyles,
        classNames?.indicatorsContainer?.(state)
      ),
    indicatorSeparator: (state) =>
      cn(
        selectStyles.indicatorSeparatorStyles,
        classNames?.indicatorSeparator?.(state)
      ),
    input: (state) => cn(classNames?.input?.(state)),
    loadingIndicator: (state) =>
      cn(
        selectStyles.loadingIndicatorStyles,
        classNames?.loadingIndicator?.(state)
      ),
    loadingMessage: (state) =>
      cn(
        selectStyles.loadingMessageStyles,
        classNames?.loadingMessage?.(state)
      ),
    menu: (state) => cn(selectStyles.menu, classNames?.menu?.(state)),
    menuList: (state) => cn(classNames?.menuList?.(state)),
    menuPortal: (state) => cn(classNames?.menuPortal?.(state)),
    multiValue: (state) =>
      cn(selectStyles.multiValueStyles, classNames?.multiValue?.(state)),
    multiValueLabel: (state) => cn(classNames?.multiValueLabel?.(state)),
    multiValueRemove: (state) => cn(classNames?.multiValueRemove?.(state)),
    noOptionsMessage: (state) =>
      cn(
        selectStyles.noOptionsMessageStyles,
        classNames?.noOptionsMessage?.(state)
      ),
    option: (state) =>
      cn(
        selectStyles.optionStyles.base,
        state.isFocused && selectStyles.optionStyles.focus,
        state.isDisabled && selectStyles.optionStyles.disabled,
        state.isSelected && selectStyles.optionStyles.selected,
        classNames?.option?.(state)
      ),
    placeholder: (state) =>
      cn(selectStyles.placeholderStyles, classNames?.placeholder?.(state)),
    singleValue: (state) => cn(classNames?.singleValue?.(state)),
    valueContainer: (state) =>
      cn(
        selectStyles.valueContainerStyles,
        classNames?.valueContainer?.(state)
      ),
  };
};

export const defaultClassNames = createClassNames({});
export const defaultStyles: StylesConfig<
  OptionType,
  boolean,
  GroupBase<OptionType>
> = {
  input: (base) => ({
    ...base,
    'input:focus': {
      boxShadow: 'none',
    },
  }),
  multiValueLabel: (base) => ({
    ...base,
    whiteSpace: 'normal',
    overflow: 'visible',
  }),
  control: (base) => ({
    ...base,
    transition: 'none',
    // minHeight: '2.25rem', // we used !min-h-9 instead
  }),
  menuList: (base) => ({
    ...base,
    '::-webkit-scrollbar': {
      background: 'transparent',
    },
    '::-webkit-scrollbar-track': {
      background: 'transparent',
    },
    '::-webkit-scrollbar-thumb': {
      background: 'hsl(var(--border))',
    },
    '::-webkit-scrollbar-thumb:hover': {
      background: 'transparent',
    },
  }),
};

/**
 * React select custom components
 */
export const DropdownIndicator = (
  props: DropdownIndicatorProps<OptionType>
) => {
  return (
    <components.DropdownIndicator {...props}>
      <ChevronDown className='h-4 w-4 opacity-50' />
    </components.DropdownIndicator>
  );
};

export const ClearIndicator = (props: ClearIndicatorProps<OptionType>) => {
  return (
    <components.ClearIndicator {...props}>
      <X className='h-4 w-4 opacity-50' />
    </components.ClearIndicator>
  );
};

export const MultiValueRemove = (props: MultiValueRemoveProps<OptionType>) => {
  return (
    <components.MultiValueRemove {...props}>
      <X className='h-3.5 w-3.5 opacity-50' />
    </components.MultiValueRemove>
  );
};

export const Option = (props: OptionProps<OptionType>) => {
  return (
    <components.Option {...props}>
      <div className='flex items-center justify-between'>
        <div>{props.label}</div>
        {props.isSelected && <Check className='h-4 w-4 opacity-50' />}
      </div>
    </components.Option>
  );
};

// Using Menu and MenuList fixes the scrolling behavior
export const Menu = (props: MenuProps<OptionType>) => {
  return <components.Menu {...props}>{props.children}</components.Menu>;
};

export const MenuList = (props: MenuListProps<OptionType>) => {
  const { children, maxHeight } = props;

  const childrenArray = React.Children.toArray(children);

  const calculateHeight = () => {
    // When using children it resizes correctly
    const totalHeight = childrenArray.length * 35; // Adjust item height if different
    return totalHeight < maxHeight ? totalHeight : maxHeight;
  };

  const height = calculateHeight();

  // Ensure childrenArray has length. Even when childrenArray is empty there is one element left
  if (!childrenArray || childrenArray.length - 1 === 0) {
    return <components.MenuList {...props} />;
  }
  return (
    <List
      height={height}
      itemCount={childrenArray.length}
      itemSize={35} // Adjust item height if different
      width='100%'
    >
      {({ index, style }) => <div style={style}>{childrenArray[index]}</div>}
    </List>
  );
};

const BaseSelect = <IsMulti extends boolean = false>(
  props: Props<OptionType, IsMulti> & { isMulti?: IsMulti },
  ref: React.Ref<SelectInstance<OptionType, IsMulti, GroupBase<OptionType>>>
) => {
  const {
    styles = defaultStyles,
    classNames = defaultClassNames,
    components = {},
    ...rest
  } = props;
  const instanceId = React.useId();

  return (
    <SelectComponent<OptionType, IsMulti, GroupBase<OptionType>>
      ref={ref}
      instanceId={instanceId}
      unstyled
      filterOption={createFilter({
        matchFrom: 'any',
        stringify: (option) => option.label,
      })}
      components={{
        DropdownIndicator,
        ClearIndicator,
        MultiValueRemove,
        Option,
        Menu,
        MenuList,
        ...components,
      }}
      styles={styles}
      classNames={classNames}
      {...rest}
    />
  );
};

export default React.forwardRef(BaseSelect) as <
  IsMulti extends boolean = false,
>(
  p: Props<OptionType, IsMulti> & {
    ref?: Ref<
      React.LegacyRef<
        SelectInstance<OptionType, IsMulti, GroupBase<OptionType>>
      >
    >;
    isMulti?: IsMulti;
  }
) => ReactElement;

@dancoon
Copy link

dancoon commented Oct 14, 2024

@H7ioo How can I use achieve multi select with the Select component?

@ilkou
Copy link
Author

ilkou commented Oct 14, 2024

@H7ioo How can I use achieve multi select with the Select component?

check the official website for all the use cases, the multi select example is in the first page

Screenshot 2024-10-14 at 15 21 16

this gist proposition only adds the styling part, react-select components work as usual

@Nfinished
Copy link

Small react19 update to the default export based on to @fitimbytyqi's work (replacing deprecated React.LegacyRef with RefAttributes<T>['ref'])

export default React.forwardRef(BaseSelect) as <
  IsMulti extends boolean = false,
>(
  p: Props<OptionType, IsMulti> & {
    ref?: React.RefAttributes<
      SelectInstance<OptionType, IsMulti, GroupBase<OptionType>>
    >['ref']

    isMulti?: IsMulti
  },
) => ReactElement

@allohamora
Copy link

allohamora commented Feb 11, 2025

If you're looking for an implementation based on react-select, you can find it here:
shadcn-ui/ui#927 (comment)

@kirubz
Copy link

kirubz commented Aug 31, 2025

Any idea why mouse wheel scrolling is not working while using inside the Shadcn Sheet component?

@H7ioo
Copy link

H7ioo commented Aug 31, 2025

Any idea why mouse wheel scrolling is not working while using inside the Shadcn Sheet component?

Does it work outside the sheet?

@kirubz
Copy link

kirubz commented Sep 25, 2025

Any idea why mouse wheel scrolling is not working while using inside the Shadcn Sheet component?

Does it work outside the sheet?

Yes, it is working outside the sheet. While using in Sheet scrolling with mouse wheel is not working. we need to drag the scroller to scroll to the dropdown list

@H7ioo
Copy link

H7ioo commented Sep 25, 2025

@kirubz could you please reproduce?
It could be a modal issue try setting the modal to false

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment