Styled form with reusable components
A form built from reusable Styled* wrappers — TextField, Select, Autocomplete, a customized DatePicker and an iOS-style Switch — controlled with plain React state, each with required validation, and a ConfigProvider supplying shared label/helper styles and the date adapter.
Component Source Code
Source of each reusable Styled* wrapper used above — expand to view or copy.
/**
* The below code snippet illustrates how to create a reusable styled Textfield
* component using MUITextField, which can be used throughout the application.
*
* In this example, the component accepts all the props of MUITextField except
* 'renderError', 'variant' and 'showLabelAboveFormField', which have already
* been configured to maintain consistent styling across the application.
* Additionally, it includes a custom error message component that displays an
* error icon alongside the error message when there is an error.
*
* A similar approach can be taken to create reusable styled components for:
* - MUINumberInput
* - MUITagsInput
* - MUIPasswordInput
*/
import { Fragment, type ReactNode } from 'react';
import Typography from '@mui/material/Typography';
import PriorityHighIcon from '@mui/icons-material/PriorityHigh';
import MUITextField, { type MUITextFieldProps } from '@nish1896/mui-components/mui/textfield';
type StyledTextFieldProps = Omit<
MUITextFieldProps,
'renderError' | 'showLabelAboveFormField' | 'variant'
>;
type StyledErrorMsgProps = {
errorMessage: ReactNode;
};
const StyledErrorMsg = ({ errorMessage }: StyledErrorMsgProps) => {
return (
<Fragment>
{Boolean(errorMessage) && (
<Typography
variant="body2"
sx={{
alignItems: 'center',
display: 'flex',
gap: 0.5
}}
>
<PriorityHighIcon color="error" fontSize="small" />
{errorMessage}
</Typography>
)}
</Fragment>
);
};
const StyledTextField = (
props: StyledTextFieldProps
) => {
const { formHelperTextProps, ...rest } = props;
const {
sx: helperTextSx,
...otherFormHelperTextProps
} = formHelperTextProps ?? {};
const helperTextSxList = Array.isArray(helperTextSx)
? helperTextSx
: [];
if (helperTextSx && !Array.isArray(helperTextSx)) {
helperTextSxList.push(helperTextSx);
}
return (
<MUITextField
{...rest}
variant="standard"
showLabelAboveFormField
formHelperTextProps={{
...otherFormHelperTextProps,
sx: [
...helperTextSxList,
{ ml: 0 }
]
}}
renderError={error => (
<StyledErrorMsg errorMessage={error} />
)}
/>
);
};
export default StyledTextField;
/**
* The below code snippet illustrates how to create a reusable styled Select
* component using MUISelect, which can be used throughout the application.
*
* A similar approach can be taken to create reusable styled components for:
* - MUINativeSelect
* - MUICheckboxGroup
* - MUIRadioGroup
*
* The only difference being that for all the above components, "multiple" generic
* prop would not be included in the type definition of the styled component.
*/
import { Poppins } from 'next/font/google';
import MUISelect, {
type MUISelectProps
} from '@nish1896/mui-components/mui/select';
import type { StrNumObjOption } from '@nish1896/mui-components/types';
const poppins = Poppins({
subsets: ['latin'],
style: 'italic',
weight: '500'
});
type StyledSelectProps<
Option extends StrNumObjOption = StrNumObjOption,
LabelKey extends Extract<keyof Option, string> = Extract<keyof Option, string>,
ValueKey extends Extract<keyof Option, string> = Extract<keyof Option, string>,
Multiple extends boolean = false
> = Omit<
MUISelectProps<Option, LabelKey, ValueKey, Multiple>,
'showLabelAboveFormField'
>;
const StyledSelect = <
Option extends StrNumObjOption = StrNumObjOption,
LabelKey extends Extract<keyof Option, string> = Extract<keyof Option, string>,
ValueKey extends Extract<keyof Option, string> = Extract<keyof Option, string>,
Multiple extends boolean = false
>({
...rest
}: StyledSelectProps<Option, LabelKey, ValueKey, Multiple>) => {
return (
<MUISelect
showLabelAboveFormField
formLabelProps={{
sx: {
fontFamily: poppins.style.fontFamily,
fontWeight: 600
}
}}
{...rest}
/>
);
};
export default StyledSelect;
/**
* The below code snippet illustrates how to create a reusable styled Autocomplete
* component using MUIAutocomplete, which can be used throughout the application.
*
* A similar approach can be taken to create reusable styled components for:
* - MUIAutocompleteObject
* - MUIMultiAutocomplete
* - MUIMultiAutocompleteObject
*/
import MUIAutocomplete, {
type MUIAutocompleteProps
} from '@nish1896/mui-components/mui/autocomplete';
import type { StrObjOption } from '@nish1896/mui-components/types';
type StyledAutocompleteProps<
Option extends StrObjOption = StrObjOption,
LabelKey extends Extract<keyof Option, string> = Extract<keyof Option, string>,
ValueKey extends Extract<keyof Option, string> = Extract<keyof Option, string>,
DisableClearable extends boolean = false,
FreeSolo extends boolean = false
> = Omit<MUIAutocompleteProps<Option, LabelKey, ValueKey, true, DisableClearable, FreeSolo>, 'multiple'>;
const StyledAutocomplete = <
Option extends StrObjOption = StrObjOption,
LabelKey extends Extract<keyof Option, string> = Extract<keyof Option, string>,
ValueKey extends Extract<keyof Option, string> = Extract<keyof Option, string>,
DisableClearable extends boolean = false,
FreeSolo extends boolean = false
>({
...rest
}: StyledAutocompleteProps<Option, LabelKey, ValueKey, DisableClearable, FreeSolo>) => {
return (
<MUIAutocomplete
formHelperTextProps={{
sx: { fontColor: theme => theme.palette.info.main }
}}
multiple
{...rest}
/>
);
};
export default StyledAutocomplete;
/**
* The below snippet illustrates how to reproduce MUI's "iOS style" Switch
* customization (https://v7.mui.com/material-ui/react-switch/#customization)
* on top of `MUISwitch`, so it can be reused across the application while
* keeping `MUISwitch`'s label / helper-text / error handling.
*
* MUI's own example wraps the raw `Switch` with `styled(Switch)(...)`. We don't
* need `styled()` here: `MUISwitch` forwards `sx`, `disableRipple` and
* `focusVisibleClassName` straight to the underlying MUI `Switch`, so the same
* overrides can be supplied through `sx`. The style object is identical to the
* upstream `IOSSwitch` example (`theme.applyStyles('dark', …)` keeps it
* theme-aware in both light and dark mode).
*
* A caller-provided `sx` is merged after the iOS overrides so per-instance
* tweaks still win.
*/
import type { Theme } from '@mui/material/styles';
import MUISwitch, {
type MUISwitchProps
} from '@nish1896/mui-components/mui/switch';
const iosSwitchSx = (theme: Theme) => ({
width: 42,
height: 26,
padding: '0px',
'& .MuiSwitch-switchBase': {
padding: '0px',
margin: '2px',
transitionDuration: '300ms',
'&.Mui-checked': {
transform: 'translateX(16px)',
color: '#fff',
'& + .MuiSwitch-track': {
backgroundColor: '#65C466',
opacity: 1,
border: 0,
...theme.applyStyles('dark', {
backgroundColor: '#2ECA45'
})
},
'&.Mui-disabled + .MuiSwitch-track': {
opacity: 0.5
}
},
'&.Mui-focusVisible .MuiSwitch-thumb': {
color: '#33cf4d',
border: '6px solid #fff'
},
'&.Mui-disabled .MuiSwitch-thumb': {
color: theme.palette.grey[100],
...theme.applyStyles('dark', {
color: theme.palette.grey[600]
})
},
'&.Mui-disabled + .MuiSwitch-track': {
opacity: 0.7,
...theme.applyStyles('dark', {
opacity: 0.3
})
}
},
'& .MuiSwitch-thumb': {
boxSizing: 'border-box',
width: 22,
height: 22
},
'& .MuiSwitch-track': {
borderRadius: `${26 / 2}px`,
backgroundColor: '#E9E9EA',
opacity: 1,
transition: theme.transitions.create(['background-color'], {
duration: 500
}),
...theme.applyStyles('dark', {
backgroundColor: '#39393D'
})
}
});
const toSxArray = (sx: MUISwitchProps['sx']) =>
/* eslint-disable-next-line no-nested-ternary */
(Array.isArray(sx) ? sx : sx ? [sx] : []);
const StyledIOSSwitch = ({
sx,
formControlLabelProps,
...rest
}: MUISwitchProps) => {
const { sx: labelSx, ...otherLabelProps } = formControlLabelProps ?? {};
return (
<MUISwitch
disableRipple
focusVisibleClassName=".Mui-focusVisible"
{...rest}
formControlLabelProps={{
...otherLabelProps,
/*
* `gap` puts 12px between the switch and its label. `ml: 0` clears
* MUI's default `FormControlLabel` `-11px` left margin (meant to align a
* checkbox/switch ripple) so the switch lines up with the other fields.
* Composed as an `sx` array so array/callback `labelSx` values survive.
*/
sx: [{ gap: '12px', ml: 0 }, ...toSxArray(labelSx)]
}}
sx={[iosSwitchSx, ...toSxArray(sx)]}
/>
);
};
export default StyledIOSSwitch;
/**
* The below snippet illustrates how to create a reusable customized DatePicker
* using MUIDatePicker, which can be used throughout the application.
*
* The look is preset here — label above the field, a `dd LLL yyyy` display
* format, a rounded / tinted input, a branded calendar icon and focus outline —
* so callers only pass data props (`value`, `onValueChange`, `errorMessage`…).
* MUIDatePicker forwards every underlying MUI `DatePickerProps` (`slots`,
* `slotProps`, `format`, …), so all customization is just props — no `styled()`.
*
* A similar approach can be taken to create reusable styled components for:
* - MUITimePicker
* - MUIDateTimePicker
*/
import CalendarMonthIcon from '@mui/icons-material/CalendarMonth';
import type { PickerValidDate } from '@mui/x-date-pickers/models';
import {
MUIDatePicker,
type MUIDatePickerProps
} from '@nish1896/mui-components/mui-pickers/date';
const brandColor = '#007bff';
type StyledDatePickerProps<TDate extends PickerValidDate = PickerValidDate>
= Omit<MUIDatePickerProps<TDate>, 'showLabelAboveFormField'>;
const StyledDatePicker = <TDate extends PickerValidDate = PickerValidDate>({
slotProps,
...rest
}: StyledDatePickerProps<TDate>) => {
return (
<MUIDatePicker
showLabelAboveFormField
format="dd LLL yyyy"
slots={{ openPickerIcon: CalendarMonthIcon }}
{...rest}
slotProps={{
...slotProps,
textField: {
sx: {
/**
* MUI X pickers use their own `MuiPickers*` field classes, not the
* plain `MuiOutlinedInput-*` ones a TextField would.
*/
'& .MuiPickersInputBase-root': {
borderRadius: '12px',
bgcolor: theme => theme.palette.action.hover
},
'& .MuiPickersInputBase-root.Mui-focused .MuiPickersOutlinedInput-notchedOutline': {
borderColor: brandColor,
borderWidth: 2
},
'& .MuiInputAdornment-root .MuiSvgIcon-root': {
color: brandColor
}
}
}
}}
/>
);
};
export default StyledDatePicker;