Skip to content

Instantly share code, notes, and snippets.

@csandman
Last active October 11, 2023 10:23
Show Gist options
  • Save csandman/c687a9fb4275112f281ab9a5701457e4 to your computer and use it in GitHub Desktop.
Save csandman/c687a9fb4275112f281ab9a5701457e4 to your computer and use it in GitHub Desktop.
Chakra UI React Select

Chakra React Select

UPDATE: I finally made an NPM package for this component! It is made with TypeScript, and now has a fully customizable styling system. It is also far ahead of this wrapper at this point. Check it out here if you'd like: https://github.com/csandman/chakra-react-select

In order to use this component, you can implement it and use it like you would normally use react-select. It should accept all of the props that the original takes, however customizing the theme or the components could break this implementation. There are also a few extra things you can do with this wrapper that pull from the chakra library.

  • You can pass the size prop with either sm, md, or lg. These will reflect the sizes available on the Chakra <Input /> component (with the exception of xs because it's too small to work).
  • In your options objects, you can add the key isFixed to emulate the example in the react-select docs.
  • You can pass the colorScheme prop to the select component to change all of the selected options tags' colors. You can view the whole list of available color schemes in the Chakra docs, or if you have a custom color palette, any of the custom color names in that will be available instead.
  • Alternatively you can add the colorScheme value to any of your options objects and it will only style that option when selected.
  • You can pass isInvalid to the select component to style it like the Chakra <Input /> is styles when it recieves the same prop.
  • You can pass isInvalid or isDisabled to a <FormControl /> which surrounds this component and it will output their corresponding <Input /> stlyes.

For this example, I am including react-select/async and react-select/creatable as well as the default, but these are obviously kept as separate packages in the original in order to reduce bundle size. So if you choose to implement this wrapper in your own project I recommend either removing the ones you don't plan on using, or separating it into 3 separate imports like the original does.

Check out the demo here: https://codesandbox.io/s/chakra-ui-react-select-648uv?file=/chakra-react-select.js

I've been thinking about making this into an NPM package, but at the end of the day I don't think I fully covered every option and I think its simple enough that people should be able to copy it and tweak it to their needs. If you have any interest in this hoever, feel free to let me know!

And if you have any other questions or requests, definitely let me know. I'm sure there are some features of react-select that I missed and I definitely want to make this wrapper as good as it can be!

import React, { cloneElement, forwardRef } from "react";
import ReactSelect, { components as selectComponents } from "react-select";
import AsyncReactSelect from "react-select/async";
import CreatableReactSelect from "react-select/creatable";
import {
Flex,
Tag,
TagCloseButton,
TagLabel,
Divider,
CloseButton,
Center,
Box,
Portal,
StylesProvider,
useMultiStyleConfig,
useStyles,
useTheme,
useColorModeValue,
useFormControl,
createIcon
} from "@chakra-ui/react";
// Taken from the @chakra-ui/icons package to prevent needing it as a dependency
// https://github.com/chakra-ui/chakra-ui/blob/main/packages/icons/src/ChevronDown.tsx
const ChevronDown = createIcon({
displayName: "ChevronDownIcon",
d: "M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
});
// Custom styles for components which do not have a chakra equivalent
const chakraStyles = {
// When disabled, react-select sets the pointer-state to none
// which prevents the `not-allowed` cursor style from chakra
// from getting applied to the Control
container: (provided) => ({
...provided,
pointerEvents: "auto"
}),
input: (provided) => ({
...provided,
color: "inherit",
lineHeight: 1
}),
menu: (provided) => ({
...provided,
boxShadow: "none"
}),
valueContainer: (provided, { selectProps: { size } }) => {
const px = {
sm: "0.75rem",
md: "1rem",
lg: "1rem"
};
return {
...provided,
padding: `0.125rem ${px[size]}`
};
},
loadingMessage: (provided, { selectProps: { size } }) => {
const fontSizes = {
sm: "0.875rem",
md: "1rem",
lg: "1.125rem"
};
const paddings = {
sm: "6px 9px",
md: "8px 12px",
lg: "10px 15px"
};
return {
...provided,
fontSize: fontSizes[size],
padding: paddings[size]
};
},
// Add the chakra style for when a TagCloseButton has focus
multiValueRemove: (
provided,
{ isFocused, selectProps: { multiValueRemoveFocusStyle } }
) => (isFocused ? multiValueRemoveFocusStyle : {}),
control: () => ({}),
menuList: () => ({}),
option: () => ({}),
multiValue: () => ({}),
multiValueLabel: () => ({}),
group: () => ({})
};
const chakraComponents = {
// Control components
Control: ({
children,
innerRef,
innerProps,
isDisabled,
isFocused,
selectProps: { size, isInvalid }
}) => {
const inputStyles = useMultiStyleConfig("Input", { size });
const heights = {
sm: 8,
md: 10,
lg: 12
};
return (
<StylesProvider value={inputStyles}>
<Flex
ref={innerRef}
sx={{
...inputStyles.field,
p: 0,
overflow: "hidden",
h: "auto",
minH: heights[size]
}}
{...innerProps}
data-focus={isFocused ? true : undefined}
data-invalid={isInvalid ? true : undefined}
data-disabled={isDisabled ? true : undefined}
>
{children}
</Flex>
</StylesProvider>
);
},
MultiValueContainer: ({
children,
innerRef,
innerProps,
data,
selectProps
}) => (
<Tag
ref={innerRef}
{...innerProps}
m="0.125rem"
// react-select Fixed Options example: https://react-select.com/home#fixed-options
variant={data.isFixed ? "solid" : "subtle"}
colorScheme={data.colorScheme || selectProps.colorScheme}
size={selectProps.size}
>
{children}
</Tag>
),
MultiValueLabel: ({ children, innerRef, innerProps }) => (
<TagLabel ref={innerRef} {...innerProps}>
{children}
</TagLabel>
),
MultiValueRemove: ({ children, innerRef, innerProps, data: { isFixed } }) => {
if (isFixed) {
return null;
}
return (
<TagCloseButton ref={innerRef} {...innerProps} tabIndex={-1}>
{children}
</TagCloseButton>
);
},
IndicatorSeparator: ({ innerProps }) => (
<Divider {...innerProps} orientation="vertical" opacity="1" />
),
ClearIndicator: ({ innerProps, selectProps: { size } }) => (
<CloseButton {...innerProps} size={size} mx={2} tabIndex={-1} />
),
DropdownIndicator: ({ innerProps, selectProps: { size } }) => {
const { addon } = useStyles();
const iconSizes = {
sm: 4,
md: 5,
lg: 6
};
const iconSize = iconSizes[size];
return (
<Center
{...innerProps}
sx={{
...addon,
h: "100%",
borderRadius: 0,
borderWidth: 0,
cursor: "pointer"
}}
>
<ChevronDown h={iconSize} w={iconSize} />
</Center>
);
},
// Menu components
MenuPortal: ({ children }) => <Portal>{children}</Portal>,
Menu: ({ children, ...props }) => {
const menuStyles = useMultiStyleConfig("Menu");
return (
<selectComponents.Menu {...props}>
<StylesProvider value={menuStyles}>{children}</StylesProvider>
</selectComponents.Menu>
);
},
MenuList: ({ innerRef, children, maxHeight, selectProps: { size } }) => {
const { list } = useStyles();
const chakraTheme = useTheme();
const borderRadii = {
sm: chakraTheme.radii.sm,
md: chakraTheme.radii.md,
lg: chakraTheme.radii.md
};
return (
<Box
sx={{
...list,
maxH: `${maxHeight}px`,
overflowY: "auto",
borderRadius: borderRadii[size]
}}
ref={innerRef}
>
{children}
</Box>
);
},
GroupHeading: ({ innerProps, children }) => {
const { groupTitle } = useStyles();
return (
<Box sx={groupTitle} {...innerProps}>
{children}
</Box>
);
},
Option: ({
innerRef,
innerProps,
children,
isFocused,
isDisabled,
selectProps: { size }
}) => {
const { item } = useStyles();
return (
<Box
role="button"
sx={{
...item,
w: "100%",
textAlign: "start",
bg: isFocused ? item._focus.bg : "transparent",
fontSize: size,
...(isDisabled && item._disabled)
}}
ref={innerRef}
{...innerProps}
{...(isDisabled && { disabled: true })}
>
{children}
</Box>
);
}
};
const ChakraReactSelect = ({
children,
styles = {},
components = {},
theme = () => ({}),
size = "md",
colorScheme = "gray",
isDisabled,
isInvalid,
...props
}) => {
const chakraTheme = useTheme();
// Combine the props passed into the component with the props
// that can be set on a surrounding form control to get
// the values of `isDisabled` and `isInvalid`
const inputProps = useFormControl({ isDisabled, isInvalid });
// The chakra theme styles for TagCloseButton when focused
const closeButtonFocus =
chakraTheme.components.Tag.baseStyle.closeButton._focus;
const multiValueRemoveFocusStyle = {
background: closeButtonFocus.bg,
boxShadow: chakraTheme.shadows[closeButtonFocus.boxShadow]
};
// The chakra UI global placeholder color
// https://github.com/chakra-ui/chakra-ui/blob/main/packages/theme/src/styles.ts#L13
const placeholderColor = useColorModeValue(
chakraTheme.colors.gray[400],
chakraTheme.colors.whiteAlpha[400]
);
// Ensure that the size used is one of the options, either `sm`, `md`, or `lg`
let realSize = size;
const sizeOptions = ["sm", "md", "lg"];
if (!sizeOptions.includes(size)) {
realSize = "md";
}
const select = cloneElement(children, {
components: {
...chakraComponents,
...components
},
styles: {
...chakraStyles,
...styles
},
theme: (baseTheme) => {
const propTheme = theme(baseTheme);
return {
...baseTheme,
...propTheme,
colors: {
...baseTheme.colors,
neutral50: placeholderColor, // placeholder text color
neutral40: placeholderColor, // noOptionsMessage color
...propTheme.colors
},
spacing: {
...baseTheme.spacing,
...propTheme.spacing
}
};
},
colorScheme,
size: realSize,
multiValueRemoveFocusStyle,
// isDisabled and isInvalid can be set on the component
// or on a surrounding form control
isDisabled: inputProps.disabled,
isInvalid: !!inputProps["aria-invalid"],
...props
});
return select;
};
const Select = forwardRef((props, ref) => (
<ChakraReactSelect {...props}>
<ReactSelect ref={ref} />
</ChakraReactSelect>
));
const AsyncSelect = forwardRef((props, ref) => (
<ChakraReactSelect {...props}>
<AsyncReactSelect ref={ref} />
</ChakraReactSelect>
));
const CreatableSelect = forwardRef((props, ref) => (
<ChakraReactSelect {...props}>
<CreatableReactSelect ref={ref} />
</ChakraReactSelect>
));
export { Select as default, AsyncSelect, CreatableSelect };
@tshal0
Copy link

tshal0 commented Apr 24, 2021

Love this implementation. Trying to get it to work with Typescript enabled and failing to get some props to be recognized. If you don't mind a suggestion, it would be awesome to have a duplicate Typescript friendly gist. Thanks for contributing this!

@csandman
Copy link
Author

@valkn0t I would love to but I'm afraid I'm not the most familiar with TypeScript. I've made one component with it, and even then it was super basic. If you want to give it a shot and share here, or point me in the right direction I'd give it a try!

@bbovenzi
Copy link

bbovenzi commented May 4, 2021

@csandman I was just about to build something like this for my team. I'll try my hand at a typescript-friendly version.

Edit:
I got this one working, but had to drop theme as a prop. @valkn0t let me know if this works on your end
https://gist.github.com/bbovenzi/76a28701b7933420655925eefaa03dd5

@ejoc
Copy link

ejoc commented May 27, 2021

another typescript version based on @bbovenzi example

import React from "react";
import Select, {
  components as selectComponents,
  GroupTypeBase,
  OptionTypeBase,
  Props as SelectProps,
  SelectComponentsConfig,
  StylesConfig,
  Theme,
} from "react-select";
import {
  chakra,
  Flex,
  Tag,
  TagCloseButton,
  TagLabel,
  Divider,
  CloseButton,
  Center,
  Portal,
  StylesProvider,
  useMultiStyleConfig,
  useStyles,
  useTheme,
  useColorModeValue,
  Icon,
  RecursiveCSSObject,
  CSSWithMultiValues,
} from "@chakra-ui/react";
import { FiChevronDown } from "react-icons/fi";

interface ItemProps extends CSSWithMultiValues {
  _disabled: CSSWithMultiValues;
  _focus: CSSWithMultiValues;
}

const chakraStyles: SelectProps["styles"] = {
  input: (provided) => ({
    ...provided,
    color: "inherit",
    lineHeight: 1,
  }),
  menu: (provided) => ({
    ...provided,
    boxShadow: "none",
  }),
  valueContainer: (provided) => ({
    ...provided,
    padding: "0.125rem 1rem",
  }),
};

const chakraComponents: SelectProps["components"] = {
  // Control components
  Control: ({ children, innerRef, innerProps, isDisabled, isFocused }) => {
    const inputStyles = useMultiStyleConfig("Input", {});
    return (
      <StylesProvider value={inputStyles}>
        <Flex
          ref={innerRef}
          sx={{
            ...inputStyles.field,
            p: 0,
            overflow: "hidden",
            h: "auto",
            minH: 10,
          }}
          {...innerProps}
          {...(isFocused && { "data-focus": true })}
          {...(isDisabled && { disabled: true })}
        >
          {children}
        </Flex>
      </StylesProvider>
    );
  },
  MultiValueContainer: ({
    children,
    innerRef,
    innerProps,
    data: { isFixed },
  }) => (
    <Tag
      ref={innerRef}
      {...innerProps}
      m="0.125rem"
      variant={isFixed ? "solid" : "subtle"}
    >
      {children}
    </Tag>
  ),
  MultiValueLabel: ({ children, innerRef, innerProps }) => (
    <TagLabel ref={innerRef} {...innerProps}>
      {children}
    </TagLabel>
  ),
  MultiValueRemove: ({ children, innerRef, innerProps, data: { isFixed } }) => {
    if (isFixed) {
      return null;
    }

    return (
      <TagCloseButton ref={innerRef} {...innerProps}>
        {children}
      </TagCloseButton>
    );
  },
  IndicatorSeparator: ({ innerProps }) => (
    <Divider {...innerProps} orientation="vertical" opacity="1" />
  ),
  ClearIndicator: ({ innerProps }) => (
    <CloseButton {...innerProps} size="sm" mx={2} />
  ),
  DropdownIndicator: ({ innerProps }) => {
    const { addon } = useStyles();

    return (
      <Center
        {...innerProps}
        sx={{
          ...addon,
          h: "100%",
          borderRadius: 0,
          borderWidth: 0,
          cursor: "pointer",
        }}
      >
        <Icon as={FiChevronDown} h={5} w={5} />
      </Center>
    );
  },
  // Menu components
  MenuPortal: ({ children }) => <Portal>{children}</Portal>,
  Menu: ({ children, ...props }) => {
    const menuStyles = useMultiStyleConfig("Menu", {});
    return (
      <selectComponents.Menu {...props}>
        <StylesProvider value={menuStyles}>{children}</StylesProvider>
      </selectComponents.Menu>
    );
  },
  MenuList: ({ innerRef, children, maxHeight }) => {
    const { list } = useStyles();
    return (
      <chakra.div
        sx={{
          ...list,
          maxH: `${maxHeight}px`,
          overflowY: "auto",
        }}
        ref={innerRef}
      >
        {children}
      </chakra.div>
    );
  },
  GroupHeading: ({ innerProps, children }) => {
    const { groupTitle } = useStyles();
    return (
      <chakra.div sx={groupTitle} {...innerProps}>
        {children}
      </chakra.div>
    );
  },
  Option: ({ innerRef, innerProps, children, isFocused, isDisabled }) => {
    const { item } = useStyles();
    return (
      <chakra.div
        role="button"
        sx={{
          ...item,
          w: "100%",
          textAlign: "left",
          bg: isFocused
            ? (item as RecursiveCSSObject<ItemProps>)._focus.bg
            : "transparent",
          ...(isDisabled && (item as RecursiveCSSObject<ItemProps>)._disabled),
        }}
        ref={innerRef}
        {...innerProps}
        {...(isDisabled && { disabled: true })}
      >
        {children}
      </chakra.div>
    );
  },
};

export function MultiSelect<
  OptionType extends OptionTypeBase = { label: string; value: string },
  IsMulti extends boolean = false,
  GroupType extends GroupTypeBase<OptionType> = GroupTypeBase<OptionType>
>({
  name = "",
  styles = {},
  components = {},
  theme,
  ...props
}: SelectProps<OptionType, IsMulti, GroupType>) {
  const chakraTheme = useTheme();
  const placeholderColor = useColorModeValue(
    chakraTheme.colors.gray[400],
    chakraTheme.colors.whiteAlpha[400]
  );

  return (
    <Select
      name={name}
      components={{
        ...(chakraComponents as SelectComponentsConfig<
          OptionType,
          IsMulti,
          GroupType
        >),
        ...components,
      }}
      styles={{
        ...(chakraStyles as StylesConfig<OptionType, IsMulti, GroupType>),
        ...styles,
      }}
      theme={(baseTheme) => ({
        ...baseTheme,
        borderRadius: chakraTheme.radii.md,
        colors: {
          ...baseTheme.colors,
          neutral50: placeholderColor, // placeholder text color
          neutral40: placeholderColor, // noOptionsMessage color
          ...(theme as Theme)?.colors,
        },
        spacing: {
          ...baseTheme.spacing,
          ...(theme as Theme)?.spacing,
        },
      })}
      {...props}
    />
  );
}

export default MultiSelect;

@csandman
Copy link
Author

@bbovenzi @ejoc Nice, I'm glad people were interested enough in this to give it a shot! I actually implemented some of your changes in the vanilla version, because I realized typescript was showing me where I did a few things wrong.

I also made a few other changes to the base version, including adding a size prop (can be set to sm, md, or lg, and match the chakra <Input>) as well as adding an async version, and fixing a few issues with keyboard navigation.

Just thought I'd let you know in case you want to integrate any of the changes with your typescript versions!

@tshal0
Copy link

tshal0 commented May 28, 2021

@bbovenzi @ejoc thanks for the Typescript implementation! Works beautifully. Sorry I couldn't get around to developing it myself; our product just launched and I've been putting out fires left and right πŸ˜…

@tcolinpa
Copy link

tcolinpa commented Jun 8, 2021

@csandman @bbovenzi thanks for this implementation :-)

@tommerkle1
Copy link

Any simple way to update the color if each individual item that has been selected? Instead of the default grey.

@csandman
Copy link
Author

csandman commented Jun 25, 2021

@tommerkle1 I had already been working on a way to add the colorScheme prop to the select itself, but I went ahead and added it to the options as well. If you take the updated gist and add the key colorScheme to any of the options you want to change (from your custom palette or the official chakra palette) it should be reflected when the options are selected.

You can also add the colorScheme prop to the select component as a whole to change all of the selected options' colors.

If you want to see how I did it, look at the MultiValueContainer custom component to see where I'm grabbing the color scheme from.

I updated my example here: https://codesandbox.io/s/chakra-ui-react-select-648uv?file=/chakra-react-select.js

@csandman
Copy link
Author

@tcolinpa no problem, glad to help! 😁

@filipesmedeiros
Copy link

@ejoc why not use the "native" @chakra-ui/icons? πŸ˜„

@csandman
Copy link
Author

@ejoc why not use the "native" @chakra-ui/icons? πŸ˜„

It was probably just what he used in the project he was implementing it in, you can change it out for whatever if you want to implement this in TS

@ejoc
Copy link

ejoc commented Jul 22, 2021

@ejoc why not use the "native" @chakra-ui/icons? πŸ˜„

Is up to you which Icon library to use

@kotoyama
Copy link

kotoyama commented Aug 9, 2021

@csandman Hi, thanks for your work! What about custom props? How to apply them to react-select + chakra correctly? For example, isInvalid prop for styling border color.

@eyenalxai
Copy link

@ejoc Looks really great, works really well on its own!
I'm trying to use with reach-hook-form like this:

<Controller
  name="select-stuff"
  defaultValue={defaultValue}
  control={control}
  render={({ field }) => (
    <MultiSelect
      {...field}
      isMulti
      options={options}
    />
  )}
/>

But get this error in console:

Warning: Function components cannot be given refs. 
Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
Check the render method of `Controller`.
    at MultiSelect (http://localhost:3000/static/js/main.chunk.js:2572:3)

Works fine if I use Select component from react-select. Is there any quick fix maybe? Thanks!

@smaven
Copy link

smaven commented Aug 21, 2021

Thanks @csandman / @ejoc !

@yatxone, you need to use forwardRef like:

export const MultiSelect = forwardRef(
  <
    OptionType extends OptionTypeBase = { label: string; value: string },
    IsMulti extends boolean = false,
    GroupType extends GroupTypeBase<OptionType> = GroupTypeBase<OptionType>,
  >(
    {
      name = '',
      styles = {},
      components = {},
      theme,
      ...props
    }: SelectProps<OptionType, IsMulti, GroupType>,
    ref: ForwardedRef<any>,
  ) => {
    const chakraTheme = useTheme();
    const placeholderColor = useColorModeValue(
      chakraTheme.colors.gray[400],
      chakraTheme.colors.whiteAlpha[400],
    );

    return (
      <Select
        ref={ref}
        // ...rest of the props
      />
    );
  },
);

@csandman
Copy link
Author

csandman commented Aug 23, 2021

@yatxone exactly what @smaven said, I made a lot of changes to this wrapper since @ejoc made his TS version, including forwarding the ref to the select component itself.

At this point I should probably make my own TS version and keep it up to date as I tweak the original, as a lot of people seem interested in it.

@csandman
Copy link
Author

csandman commented Aug 23, 2021

@kotoyama sorry for the late response, I didn't see your comment! You mentioned isInvalid and that got me thinking that it is something I should implement. My wrapper now accepts the isInvalid prop on both the <Select /> element itself, or a surrounding <FormControl /> element instead, should you choose to implement it that way.

While I was at it, I also added the option to pass isDisabled to a surrounding <FormControl /> in order to disable it!

If you look at my revision (should be the third to most recent) you can see what I changed to make that work!

Also, you said custom props, and I'm not sure if there was anything else you wanted to add, but you can pass any props you want into the main <Select /> component, and if you want to use it on one of the specific sub-components you can grab it from the prop selectProps.

See line 101 for an example: https://gist.github.com/csandman/c687a9fb4275112f281ab9a5701457e4#file-chakra-react-select-js-L101

Let me know if you have any other questions and I'll try to respond faster this time!

@kotoyama
Copy link

kotoyama commented Sep 5, 2021

@csandman you saved me :) Thanks again! I think that this implementation might be a great separate package πŸ‘

@baaraak
Copy link

baaraak commented Sep 12, 2021

Thank you for this!
One little thing, maybe you can change in line 255
https://gist.github.com/csandman/c687a9fb4275112f281ab9a5701457e4#file-chakra-react-select-js-L255
I need rtl support so I changed textAlign="left" to textAlign="start" that way it will work for both rtl and ltr.

@csandman
Copy link
Author

@baaraak thats a good call, thanks for the recommendation! I made the change in the gist and in the demo on codesandbox.

@waraness99
Copy link

Thanks! This implementation would be a great package

@csandman
Copy link
Author

@kotoyama @waraness99 I finally got a published version of the package working! Check it out if you get a chance!

https://www.npmjs.com/package/chakra-react-select

@waraness99
Copy link

@kotoyama @waraness99 I finally got a published version of the package working! Check it out if you get a chance!

https://www.npmjs.com/package/chakra-react-select

Omg! So cool, thanks a lot @csandman!

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