Custom DatePicker with modified slots

QuantumSoft
9 min readJan 19, 2024

By Yuriy Axenov, Senior Frontend Developer ar QuantumSoft

For better convenience and comprehension of the further text, you can first have a look at the code in GitHub repository by clicking here: github repository with code

This is a common scenario where all elements in a project should consistently function and appear in a specific style. The MUI framework provides a variety of capabilities to fulfill such requirements. In this article, we will explore methods for customizing components as a whole and address a non-trivial situation involving the DatePicker component. To illustrate this, we will implement a single-page application where the distinction between the standard display and the modified version will be evident.

We will use Vite, as it is a simple and lightweight solution for launching React applications.

yarn create vite app --template react-ts

Let’s configure aliases for the src folder in project.

yarn add -D @types/node

vite.config.ts

...
alias: {
'@': path.resolve(__dirname, './src'),
},
...

tsconfig.json

... 
"paths": {"@/*": ["./src/*"] },
...

We will use the Sass preprocessor for working with styles.

yarn add -D sass

Let’s install all the necessary dependencies for working with MUI and the MUI DatePicker component.

yarn add @mui/material @emotion/react @emotion/styled
yarn add @mui/x-date-pickers
yarn add dayjs

The next step is to set up a theme, which is one of the options for customizing components in MUI.

I would like the colors used in the theme to be accessible in CSS files when needed, so let’s add them as CSS variables and then use them in the theme object. To achieve this, we’ll implement a scheme with variables.module + variables files, allowing us to obtain values in JavaScript code and simultaneously set these values in :root.

variables.module.scss

...
:export {
primary_color: $primary-color;
secondary_color: $secondary-color;
accent_color: $accent-color;
...
}

variables.scss

@use './variables.module' as *;
:root {
--primary-color: #{$primary-color};
--secondary-color: #{$secondary-color};
--accent-color: #{$accent-color};
...
}

In our simple example, we will focus solely on modifying the color scheme. However, MUI provides the flexibility to customize a wide variety of parameters by configuring the theme. This practical approach allows you to tailor your application to a designer’s specified style without having to make direct changes to the components themselves. Therefore, it is advisable to explore this approach before considering creating your own customizations. In your case, working with the theme may fulfill all your requirements.

const theme = createTheme({
palette: {
primary: {
main: variables.primary_color,
},
secondary: {
main: variables.secondary_color,
},
info: {
main: variables.info_color,
},
warning: {
main: variables.accent_color,
},
background: {
default: variables.background_color,
},
error: {
main: variables.error_color,
},
text: {
primary: variables.text_color,
secondary: variables.secondary_text_color,
},
},

The theme object itself is not enough for the theme to take effect. Therefore, let’s make some changes to the main.tsx file. If you only want to apply the theme to specific pages, you can use the ThemeProvider on those particular pages where you want it to take effect. In our case, we will apply the theme to the entire application.

main.tsx

...
<ThemeProvider theme={theme}>
<CssBaseline />
<App />
</ThemeProvider>
...

It’s time to write our first components. As a first step, let’s write a styled Input. In reality, we’ll just change the padding, but that’s enough to understand how it works.

Input.tsx

import MuiInput from '@mui/material/Input';
import { styled } from '@mui/material/styles';

const Input = styled(MuiInput)`
& .MuiInput-input {
padding: 10px 12px;
}
`;

export default Input;

MUI provides us with the ability to write CSS code inside a JS file through the @emotion/styled library. When modifying styles for standard MUI components, it’s important to understand which class we want to alter. To find out the classes associated with a component you can refer to the MUI documentation.

The DatePicker component looks similar to the TextField component before interacting with it. To clearly see that we are using the modified TextField component, we will need to implement it first.

In practice, TextField is a complex component composed of various elements. While modifying the theme or CSS properties with Emotion is a great option, let’s consider a scenario where we want to utilize our new Input.tsx within a styled TextField. In this case, we would need to manually recreate the structure of the original TextField. Although the structure in this example may not match the original MUI TextField component exactly, we will cover the main elements.

The structure of the our TextField.

<FormControl>
<InputLabel>...</...>
<Input />
</...>

FormControl is the root element in the structure tree and is responsible for defining how the TextField will appear.

InputLabel is responsible for rendering the label of the field.

Input is the component where actual input values will be entered, and we have already created it.

const TextFieldRoot = styled(FormControl)`
& .MuiInputBase-root {
background-color: var(--input-bg-color);
border: 1px solid var(--input-border-color);
border-radius: 2px;
box-sizing: border-box;

${(props) =>
props.error &&
`
--input-border-color: var(--error-color);
--input-border-color-hover: var(--error-color);
--primary-color: var(--error-color);
`}

&:hover:not(.Mui-disabled) {
border-color: var(--input-border-color-hover);
}

&:focus-within:not(.Mui-disabled) {
background-color: unset;
border-color: var(--primary-color);
}
}

& .MuiInputBase-root.MuiInputBase-adornedStart {
.MuiInputBase-input {
padding-left: 0;
}
}

& .MuiInputBase-root.MuiInputBase-adornedEnd {
.MuiInputBase-input {
padding-right: 0;
}
}

& .MuiInputAdornment-positionStart {
padding-left: 12px;
}

& .MuiInputAdornment-positionEnd {
padding-right: 12px;
}

& .MuiInput-root .MuiSelect-icon {
right: 4px;
}
`;

We’re customizing the appearance of our component by adjusting colors and padding for different states. When rewriting styles, it’s important to note that theme values won’t be applied automatically unless we reference them directly. In the example, I use CSS variables matching theme values. However, MUI allows direct access to theme values for better component flexibility.

interface TextFieldProps extends InputProps {
label?: React.ReactNode;
containerRef?: React.Ref<HTMLDivElement>;
}

const TextField: React.FC<TextFieldProps> = forwardRef((props, inputRef) => {
const id = useId();
const { containerRef, ...rest } = props;
return (<TextFieldRoot
variant="standard"
>
{props.label && (
<InputLabel id={`label-${id}`} shrink htmlFor={id}>
{props.label}
</InputLabel>
)}
<Input
{...rest}
disableUnderline
id={id}
inputRef={inputRef}
ref={containerRef}
/>
</TextFieldRoot>);
});

Now the visual difference is apparent.

The preparation is complete, and now we can start working with the DatePicker component itself.

To begin, let’s display the standard DatePicker without any modifications. It’s important to note that we are using the dayjs library for date manipulation, so we need to explicitly use the dayjs adapter through the LocalizationProvider.

<LocalizationProvider dateAdapter={AdapterDayjs}>
<Box sx={{ width: '250px' }}>
<DatePicker label="Label" />
</Box>
</LocalizationProvider>

Our next objective is to display a DatePicker component that utilizes our customized TextField.

To achieve this, we will take advantage of the recently added slots mechanism in MUI, which allows us to modify the default behavior and appearance of components. The slots parameter is available for most complex components in MUI, including the DatePicker. By referring to the documentation, we can explore the available slots for the component and gain a better understanding of the purpose of each specific slot.

After analyzing the documentation, one of these two options is likely to be suitable for us.

textField — form control with an input to render the
value inside the default field.

field — component used to enter the date with the keyboard.

It seems that the field slot is the most suitable for achieving our goal. However, the textField slot could also work for our purposes, making everything straightforward.

Let’s try to modify the DatePicker using the textField slot. We encounter a type mismatch error right away. The reason is that our version of TextField actually has much more limited functionality than the original TextField and uses completely different props (based on InputProps rather than TextFieldProps). But let's assume that we want to use the component we already have. This is a good example if we want to use TextField that we wrote entirely on our own or a TextField built on another library. In such a case, we would have to use a more low-level slot—field.

But here too, we face a type mismatch. In this case, we need to understand what the field slot expects and write a wrapper that conforms to the expected props.

It expects a type that extends UseDateFieldProps and BaseSingleInputFieldProps. Let's declare the interface and write the wrapper accordingly.

interface DateFieldWrapperProps
extends UseDateFieldProps<Dayjs>,
BaseSingleInputFieldProps<Dayjs | null,
Dayjs,
FieldSection,
DateValidationError> { }

const DateFieldWrapper: React.FC<DateFieldWrapperProps> = (props) => {
const {
inputRef: externalInputRef,
slots,
slotProps,
...textFieldProps } = props;

const response = useDateField<Dayjs, typeof textFieldProps>({
props: textFieldProps,
inputRef: externalInputRef,
});
const {
// get rid of non available props for our Input
onClear,
clearable,
focused,

InputProps,
ref,
...others } = response;

return <STextField
containerRef={InputProps?.ref}
startAdornment={InputProps?.startAdornment}
endAdornment={InputProps?.endAdornment}
{...others}
ref={ref}
/>;
}

Implementing the wrapper is a straightforward process. We just need to handle the incoming data from the DatePicker, as we would with standard MUI components. MUI already provides the useDateFieldhook specifically for this purpose. By utilizing the responsevariable obtained from this hook, we receive a set of parameters that are ready to be passed to any of our components that will occupy the fieldslot.

It can be said that we have solved our task; now we just need to specify the wrapper as the field slot for the DatePicker.

const DatePicker: React.FC<DatePickerProps<Dayjs>> = (props) => {
return (
<MuiDatePicker
{...props}
slots={{
field: DateFieldWrapper,
...props.slots,
}}
/>);
}

However, we need to understand what InputProps, ref, and why we are not passing whole response.

You can read more about ref in the official React documentation. In short, ref itself can represent any value. However, in our case, it is references to HTML elements that constitute the component. So, having this reference, the internal code can access this HTML element and do something with it, such as obtaining its coordinates on the screen.

InputProps represents the parameters specifically for the Input component. It's important to understand that the Input component consists not only of the <input> tag but is a tree of HTML elements where the <input> tag is nested within a parent container. This is why we specify ref from InputProps in containerRef so that the DatePicker can communicate with the container containing the <input>. However, DatePicker also uses access to the <input> tag—in our case, this is handled by the ref prop. Since we used forwardRef when creating the TextField, the value specified in ref will be available as the second parameter in the forwardRef function.

Let’s take a look on TextField compoment code.

 <Input
{...rest}
disableUnderline
id={id}
inputRef={inputRef}
ref={containerRef}
/>

The inputRef variable corresponds to the variable name used in forwardRef, which will hold the value we passed to ref in the wrapper. Similarly, the containerRef variable is a prop we are using for the TextField, so it has the same name as we used in the wrapper.

The inputRef prop in Input is for the reference to the <input> tag, so we assign it the ref from response by passing the inputRef variable. Since Input is the container, its ref should be associated with containerRef.

The most challenging part is now complete. We have also removed the onClear, clearable, and focused parameters, as our TextField does not support them. If needed, we can implement them separately, but they are not included in this example. The startAdornment and endAdornment properties are nested within the InputProps object, so we need to pass them separately.

Now, let’s examine the result. As you can see, the styled version utilizes the wrapper and showcases our customized TextField, successfully achieving our desired outcome.

  1. React API
  2. Material UI
  3. Vite
  4. MUI Input
  5. MUI TextField
  6. MUI DatePicker
  7. Github Repository with project code

The text above was prepared and accomplished by Yuriy Axenov. The opinions expressed here are the author’s own and do not necessarily represent the views of QuantumSoft.

--

--