How to style your Material UI TextField, Integrate it with React Hook Form, and Make it Reusable?

alvinIrwanto
6 min readJun 18, 2024

--

Hello guys, long time no see hahaha

I’m quite busy right now with my work. It’s a long journey because I have to work with Angular in the meantime since the company I work for uses it. So yeah, it’s challenging because I have to learn by doing to get the job done. But now, I’m back with React again, yay! So I will continue my blog here.

I actually have a plan to create a series, so it won’t be a long blog but just a mini blog that solves mini problems. Today, I want to start by creating a reusable component using TextField from MUI and integrating it with React Hook Form. In this component, I also combine it a little bit with Tailwind CSS, but you can easily change that. So, let’s get started!

First of all, of course, you need to install the library that you need, which is the MUI and React Hook Form. In this tutorial, I want to add another library which is Yup to use later for a validation schema.

npm install @mui/material @emotion/react @emotion/styled react-hook-form

After that, you can create the component and name it whatever you want.

I’m gonna create this input reusable for input like text, password, and number. First, I’m gonna define the props of these components.

export default function InputField({
type, // for type of the input
register, // props from react-hook-form
errors, // props from react-hook-form
name, // name for the field
label, // label .-.
center, // position of the input if you want to make it center
placeholder, // .-.
value, // value of the input if you want to input the default value
startAdorn, // If you want to add something in front of the field
endAdorn, // add something on the back
disabled // disabled the input
}) {

The little explanation is on the code above because it is more efficient haha. Next is a function to handle the type:

const [inputType, setInputType] = useState(type);

const togglePasswordVisibility = () => {
setInputType(prevType => prevType === 'password' ? 'text' : 'password' || type);
};

const handleKeyDown = (type, e) => {
if (type === 'number' && ['e', 'E', '+', '-', '.', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
e.preventDefault();
}
};

The function togglePasswordVisibility is to handle the type of password. “why don’t we just use type password in the Textfield directly?”

yes, you can actually, but the problem will come when there is more than 1 reusable input in the same form, for example in the confirm password form. it will show and hide the password together even though the one that clicked is only one.

The handleKeyDown function works to handle the input type number so users will only use literal numbers only, so they cannot type e, +, use the arrow key, etc.

Now let’s move to the Textfield itself, there is something a lot going on here, so I will explain also in the code:

<TextField
onKeyDown={(e) => handleKeyDown(type, e)} // this for apply the function above
label={label} // add the label from the props
{...register(name)} // register the name to react-hook-form
size="small" // I used size small here, but you can change the size by follow the MUI documentation
disabled={disabled}
value={value}
type={inputType}
placeholder={placeholder}
fullWidth
inputProps={{
// the styling if you want to make your input with center align
style: { textAlign: `${center ? 'center' : 'left'}` },
}}
InputProps={{
// style the font size and height
style: { fontSize: 15, height: '2.5rem' },
startAdornment: startAdorn && (
<InputAdornment position="start">
{startAdorn}
</InputAdornment>
),
endAdornment: endAdorn ? (
<InputAdornment position="end">
<div style={{ fontSize: '14px' }}>
{endAdorn}
</div>
</InputAdornment>

// this is for password type, so there will be an eye icon to change
// from password to text vise versa
) : type === 'password' ? (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={togglePasswordVisibility}
edge="end"
>
{inputType === 'password' ? <VisibilityOffOutlinedIcon className='text-base' /> : <VisibilityOutlinedIcon className='text-base' />}
</IconButton>
</InputAdornment>
) : ''
}}
aria-describedby="outlined-weight-helper-text"
sx={{
textAlign: 'center !important',
// This is the style for the color text when it disabled
"& .MuiInputBase-input.Mui-disabled": {
WebkitTextFillColor: "#000000",
},
// This is the root base style how your field would look like
"& .MuiOutlinedInput-root": {
color: "#000",
background: `${errors ? '#ff000012' : 'white'}`,
borderRadius: '5px',
// This is for the outline
"& .MuiOutlinedInput-notchedOutline": {
borderColor: `${errors ? 'red' : '#E7E9EB'}`,
borderWidth: "1px",
},
// Style when the field is focused
"&.Mui-focused": {
"& .MuiOutlinedInput-notchedOutline": {
borderColor: `${errors ? 'red' : '#4F4F4F'}`,
borderWidth: "2px",
},
},
// This is when is is not focused
"&:hover:not(.Mui-focused)": {
"& .MuiOutlinedInput-notchedOutline": {
borderColor: `${errors ? 'red' : '#E7E9EB'}`,
borderWidth: `${errors ? '2px' : ''}`,
},
},
},
// Style for the label
"& .MuiInputLabel-outlined": {
color: "#4F4F4F",
fontSize: 15,
// And the label when it focused
"&.Mui-focused": {
color: `${errors ? 'red' : '#4F4F4F'}`,
fontSize: 15
},
},
}}
/>

And then last part is to add an information when there is error information:

{
errors && (
<div className="flex justify-start items-start gap-[5px] text-rose-500 mt-[3px]">
<InfoOutlinedIcon className="text-sm mt-[3px]" />
<Typography variant="caption" className="mt-[1px]">{errors}</Typography>
</div>

)
}

As I said, I used a combination with Tailwind CSS in my project, if you want to change it, you can use the CSS or Styled component.

So the whole code will look like this

import { InputAdornment, TextField, Typography } from '@mui/material'
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import IconButton from '@mui/material/IconButton';
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined';
import { useState } from 'react';

export default function InputField({
type,
register,
errors,
name,
label,
center,
placeholder,
value,
startAdorn,
endAdorn,
disabled
}) {

const [inputType, setInputType] = useState(type);

const togglePasswordVisibility = () => {
setInputType(prevType => prevType === 'password' ? 'text' : 'password' || type);
};

const handleKeyDown = (type, e) => {
if (type === 'number' && ['e', 'E', '+', '-', '.', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
e.preventDefault();
}
};


return (
<div className='flex flex-col'>
<TextField
onKeyDown={(e) => handleKeyDown(type, e)}
label={label}
{...register(name)}
size="small"
disabled={disabled}
value={value}
type={inputType}
placeholder={placeholder}
fullWidth
inputProps={{
style: { textAlign: `${center ? 'center' : 'left'}` },
}}
InputProps={{
style: { fontSize: 15, height: '2.5rem' },
startAdornment: startAdorn && (
<InputAdornment position="start">
{startAdorn}
</InputAdornment>
),
endAdornment: endAdorn ? (
<InputAdornment position="end">
<div style={{ fontSize: '14px' }}>
{endAdorn}
</div>
</InputAdornment>
) : type === 'password' ? (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={togglePasswordVisibility}
edge="end"
>
{inputType === 'password' ? <VisibilityOffOutlinedIcon className='text-base' /> : <VisibilityOutlinedIcon className='text-base' />}
</IconButton>
</InputAdornment>
) : ''
}}
aria-describedby="outlined-weight-helper-text"
sx={{
textAlign: 'center !important',
"& .MuiInputBase-input.Mui-disabled": {
WebkitTextFillColor: "#000000",
},
"& .MuiOutlinedInput-root": {
color: "#000",
background: `${errors ? '#ff000012' : 'white'}`,
borderRadius: '5px',

"& .MuiOutlinedInput-notchedOutline": {
borderColor: `${errors ? 'red' : '#E7E9EB'}`,
borderWidth: "1px",
},
"&.Mui-focused": {
"& .MuiOutlinedInput-notchedOutline": {
borderColor: `${errors ? 'red' : '#4F4F4F'}`,
borderWidth: "2px",
},
},
"&:hover:not(.Mui-focused)": {
"& .MuiOutlinedInput-notchedOutline": {
borderColor: `${errors ? 'red' : '#E7E9EB'}`,
borderWidth: `${errors ? '2px' : ''}`,
},
},
},
"& .MuiInputLabel-outlined": {
color: "#4F4F4F",
fontSize: 15,

"&.Mui-focused": {
color: `${errors ? 'red' : '#4F4F4F'}`,
fontSize: 15
},
},
}}
/>
{
errors && (
<div className="flex justify-start items-start gap-[5px] text-rose-500 mt-[3px]">
<InfoOutlinedIcon className="text-sm mt-[3px]" />
<Typography variant="caption" className="mt-[1px]">{errors}</Typography>
</div>

)
}
</div>
)
}

And now, how to use it??

This is the example how to use that

<form>
<InputField
name='username'
register={register}
errors={errors?.username?.message}
/>
<InputField
label="Password"
name="password"
type="password"
register={register}
errors={errors?.password?.message}
/>
</form>

And you can combine it with yup validation:

const schema = yup.object().shape({
username: yup.string().required("username is required!"),
password: yup.string().required("Password is required!"),
})

const {
register,
handleSubmit,
watch,
formState: { isValid, errors },
} = useForm({
resolver: yupResolver(schema),
mode: "onTouched",
})

For other examples, don’t worry I’ll create another one and use the same input field component from here, so stay tuned on this series.

Thank you!!

--

--