The Ultimate MUI Select Component Tutorial (With TypeScript!)

The MUI Select component is an input/dropdown combo that comes with dozens of configurable props. In this tutorial I will customize the dropdown position, the default and placeholder values, add multiselect, and add labels and helper text, and more.

MUI Select with Dropdown Offset and Placeholder Value
MUI Select with Dropdown Offset and Placeholder Value

In most of my examples I dive deep into styling, but this tutorial is all about functionality. Read this post to learn how to style the Select component.

Single select and multiselect are significantly different in their code and TypeScript typings. I will first build single select, but I will mention differences with multiselect and then elaborate on them later in the article.

All code for the Select demo is in the Resources section, plus links to docs and related resources.

Watch the YouTube version of this demo here, or below:

Material-UI Select onChange TypeScript Typing

In this example we are building a Select component that has a dropdown of “Score” options. The single select version accepts a string value, but I used an array of strings simply to render the Select:

const [score, setScore] = useState('');
const scoreData = ["100", "90", "80", "70", "60"];

One of the first steps in constructing a Select component is to add the onChange handler. Here is the code for single select:

const handleChange = (event: SelectChangeEvent<string>, child: React.ReactNode) => {
    setScore(event.target.value);
  };

event.target.value is a string value by default. Also notice that there is the option to pass a second parameter which is the ReactNode that was clicked. It has useful info such as the text value of the node.

Multiselect will have significantly different typing because it’s score state value is an array of strings. We’ll see this further below.

Material-UI Select Label and Helper Text

The MUI Select component is like an Input component with a Menu dropdown. However, we can add a Label and Helper text manually. I’ll demonstrate this below, but you can also accomplish this simply by using a TextField and setting prop select={true} on it, turning the TextField into a Select with full TextField props.

<FormControl sx={{width: 200}}>
  {/* Supplies text for label */}
  <InputLabel id="custom-select-label">Score</InputLabel>
  {/* Label prop manages gap in Select border*/}
  <Select label={score ? "Score" : ""}/>
  <FormHelperText sx={{marginLeft: 0}}>With TypeScript!</FormHelperText>
</FormControl>

Interestingly, we need both the InputLabel (for the text) and the label prop (for notifying the Select that there will be a label).

In the next section I will conditionally render these if there is no default value and a placeholder gets rendered.

The FormHelperText renders nicely right below the Select if everything is wrapped by a FormControl. The InputLabel and FormHelperText need to be rendered inside a FormControl in order to have the nice behavior and positioning that we see in the MUI documentation.

MUI Select Placeholder and Default Value

Placeholders and default values are similar concepts. The placeholder is usually explanatory text (“Nothing Selected” in my example) and may have unique styling. A default value is more likely to be a useful pre-selected value from the list, for example if I initialized my score value to 100.

Here’s the code to initialize a default value in the state prop:

const [score, setScore] = useState('100');

That’s all it takes. With the above code, 100 will render in the input box on page render and any components or functionality depending on the score state will observe that it has a value.

Interestingly, I can set a default value that is not in the list. Here’s the below screenshot where I set the value to 12.

MUI Select with default value
MUI Select with default value

Placeholder text requires more code. The select component supplies a useful prop call renderValue that allows us to customize how values are rendered in the input area. It can render text or an element.

const [score, setScore] = useState('');

//Select prop
renderValue={(value) => value ? value : <em>Nothing Selected</em>}

I added code that evaluates the value of the Select and renders it if it exists or renders emphasized text of “Nothing Selected” if the value does not exist. The only time that nothing is selected is on render if I set an empty string as the state value. Later I will also add a Clear button that resets the value.

If you use a Label, as I did, you will need to conditionally render it only when the Select component has a value. Otherwise you get overlap:

MUI Label needs to be conditionally rendered

The same is true for the Select’s label prop. Here’s the code I wrote, but it’s one of many ways to accomplish this:

//JSX for InputLabel
{score ? <InputLabel id="custom-select-label">Score</InputLabel> : ''}

//JSX for Select
<Select
  label={score ? "Score" : ""}
/>

This is the code for single select. If I enable multiselect, score will be an array and I need to check it’s length and only render labels when score array is populated.

Material-UI Select Dropdown Position

The position of the Menu dropdown can be customized by setting a position value such as left, right, top, or bottom. The difficulty is these have to be passed through a series of layers to be properly applied:

MenuProps={{
  PaperProps: {sx: {left: `10px !important`}}
}}

The Paper component is the outermost layer of the Menu in the DOM. To pass props to the Paper component, we first must pass through the MenuProps.

Also, I had to add !important. I always try to avoid that, but MUI occasionally uses inline styling. Since the sx prop creates a class, and inline styling has higher precedence than classes, we have to use !important. See the DOM screenshot below:

MUI Select Menu DOM
MUI Select Menu DOM

You can see the left value in the inline styling is overwritten (but you can’t see the left value I passed).

Also, in the screenshot above I didn’t pass a value of 10px. I passed a dynamic value that finds the Select’s left position in the viewport and adds 30. The core code is below.

  const inputComponent = useRef<HTMLInputElement>(null);
  const [position, setPosition] = useState(0);

  useEffect(() => {
    setPosition(inputComponent.current? (inputComponent.current.getBoundingClientRect().left + 30): 0);
  }, [inputComponent]);

//JSX return
<Select
  ref={inputComponent}
  MenuProps={{
    PaperProps: {sx: {left: `${position}px !important`}}
  }}
/>

This code adds a ref to the Select, consumes the value in a useEffect, and sets it to a useState value. The useEffect and useState are necessary. Without them, sometimes the Select was not in the correct position when the inputComponent ref’s left position was calculated. Probably the browser had not finished all the render calculations.

If you are using TypeScript, make sure to set the useRef type to <HTMLInputElement>. Without this, getBoundingClientRect is not available to call.

Material-UI Select Options

Options are another name for the menu items inside a Select component. As mentioned, I rendered these dynamically from an array of string values.

You might see examples, including in the MUI docs, where the developer adds a placeholder Option with no value:

<MenuItem disabled value="">
  Nothing!
</MenuItem>

This adds a permanent placeholder value in the dropdown list. Unless you desire to have a disabled value in the dropdown, I recommend following my practice above where I add a placeholder with renderValue and its only visible until the first value is selected.

MUI Select Dropdown maxHeight and Scrollbar

If you have lots of options in the dropdown, the dropdown will expand vertically to show them.

MUI Select Dropdown maxHight
MUI Select Dropdown maxHight

You can limit this by passing a maxHeight value like the code below. A scrollbar will auto render when necessary.

MenuProps={{
  PaperProps: { sx: { maxHeight: 200 }}
}}

Material-UI MultiSelect

MUI Select with Multiselect, Select All, and Clear

Multiselect is conceptually the same as single select, but everything operates through an array instead of a string. As such, our conditional rendering for labels and placeholder text must be updated to test for score length.

The TypeScript typing is quite a bit trickier. The handleChange function I pulled straight from the MUI docs because they beautifully extract the value from the clickHandler.

Here’s the new code for the state const. Notice the string array.

const [score, setScore] = React.useState<string[]>([]);

Next we have the Select props that have changed and the conditional rendering of the InputLabel. Notice how we check for length to be truthy.

{score.length ? <InputLabel id="custom-select-label">Score</InputLabel> : ''}
<Select
  label={score.length ? "Score" : ""}
  renderValue={(values: Array<string>) => values.length ? values.join(', ') : <em>Nothing Selected</em>}
/>

Finally we have our new clickHandler. SelectChangeEvent now expects a string array.

const handleChange = (event: SelectChangeEvent<string[]>) => {
  const {
    target: { value },
  } = event;
  setScore(
    typeof value === 'string' ? value.split(',') : value,
  );
};

The full code for single select is in the Resources selection. Simply update the appropriate values with the code above to see the multiselect demo working.

MUI Select All

The following two buttons are wrapped in a Stack component.

I added a button that programmatically selects all the dropdown menu items when the button is clicked. It is actually a simple thing to do, simply pass the existing array of score data to the setScore setter.

const onSelectAllClick = () => setScore(scoreData);  //["100", "90", "80", "70", "60"]

<Button variant="outlined" onClick={onSelectAllClick}>Select All</Button>

MUI Select Clear Value

The Clear button passes an empty array to setScore.

const onClearClick = () => setScore([]);

<Button variant="outlined" onClick={onClearClick}>Clear All</Button>

Resources and Related Links

Here’s how to style the TextField border color.

Here’s all you need to know about MUI labels.

Full Code with Single Select:

import {useEffect, useRef, useState} from 'react';
import InputLabel from '@mui/material/InputLabel';
import MenuItem from '@mui/material/MenuItem';
import FormControl from '@mui/material/FormControl';
import Select, { SelectChangeEvent } from '@mui/material/Select';
import FormHelperText from '@mui/material/FormHelperText';

export default function CustomSelect() {
  const [score, setScore] = useState('');
  const inputComponent = useRef<HTMLInputElement>(null);
  const [position, setPosition] = useState(0);

  useEffect(() => {
    setPosition(inputComponent.current? (inputComponent.current.getBoundingClientRect().left + 30): 0);
  }, [inputComponent]);

  const scoreData = ["100", "90", "80", "70", "60"];

  const handleChange = (event: SelectChangeEvent<string>, child: React.ReactNode) => {
    setScore(event.target.value);
  };

  return (
      <FormControl sx={{width: 200}}>
        {/* Supplies text for label */}
        {score ? <InputLabel id="custom-select-label">Score</InputLabel> : ''}
        <Select
          ref={inputComponent}
          labelId="custom-select-label"
          id="custom-select"
          value={score}
          label={score ? "Score" : ""} //This tells Select to have gap in border
          onChange={handleChange}
          displayEmpty
          renderValue={(value) => value ? value : <em>Nothing Selected</em>}
          MenuProps={{
            PaperProps: {sx: {left: `${position}px !important`}}
          }}
        >
          {/*Don't add a placeholder, instead use renderValue to control emptry value text */}
          {scoreData.map((scoreValue) => {
            return <MenuItem value={scoreValue}>{scoreValue}</MenuItem>
          })}
        </Select>
        <FormHelperText sx={{marginLeft: 0}}>With TypeScript!</FormHelperText>
      </FormControl>
  );
}

MUI Select Docs

Share this post:

2 thoughts on “The Ultimate MUI Select Component Tutorial (With TypeScript!)”

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.