RHF-MUI Components
NPM

Styled form with reusable components

A form built from reusable Styled* wrappers — TextField, Select, Autocomplete, a customized DatePicker and an iOS-style Switch — wired to React Hook Form with registerOptions 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 RHFTextField, which can be used throughout the application.
 *
 * In this example, the component accepts all the props of RHFTextField 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:
 * - RHFNumberInput
 * - RHFTagsInput
 * - RHFPasswordInput
 */

import { Fragment, type ReactNode } from 'react';
import { type FieldValues } from 'react-hook-form';
import Typography from '@mui/material/Typography';
import RHFTextField, { type RHFTextFieldProps } from '@nish1896/rhf-mui-components/mui/textfield';
import PriorityHighIcon from '@mui/icons-material/PriorityHigh';

type StyledRHFTextFieldProps<T extends FieldValues> = Omit<
  RHFTextFieldProps<T>,
  'renderError' | 'showLabelAboveFormField' | 'variant'
>;

type StyledErrorMsgProps = {
  errorMessage: ReactNode;
};

const StyledErrorMsg = ({ errorMessage }: StyledErrorMsgProps) => {
  return (
    <Fragment>
      {!!errorMessage && (
        <Typography
          variant="body2"
          sx={{
            alignItems: 'center',
            display: 'flex',
            gap: 0.5
          }}
        >
          <PriorityHighIcon color="error" fontSize="small" />
          {errorMessage}
        </Typography>
      )}
    </Fragment>
  );
};

const StyledRHFTextField = <T extends FieldValues>(
  props: StyledRHFTextFieldProps<T>
) => {
  const { formHelperTextProps, ...rest } = props;
  const {
    sx: helperTextSx,
    ...otherFormHelperTextProps
  } = formHelperTextProps ?? {};
  const helperTextSxList = Array.isArray(helperTextSx)
    ? helperTextSx
    : [];

  if (helperTextSx && !Array.isArray(helperTextSx)) {
    helperTextSxList.push(helperTextSx);
  }

  return (
    <RHFTextField
      {...rest}
      variant="standard"
      showLabelAboveFormField
      formHelperTextProps={{
        ...otherFormHelperTextProps,
        sx: [
          ...helperTextSxList,
          { ml: 0 }
        ]
      }}
      renderError={error => (
        <StyledErrorMsg errorMessage={error?.message} />
      )}
    />
  );
};

export default StyledRHFTextField;

/**
 * The below code snippet illustrates how to create a reusable styled Select
 * component using RHFSelect, which can be used throughout the application.
 *
 * `multiple` is left in the prop surface (not `Omit`'d) so its generic
 * `Multiple` type param is inferred per call site from the literal passed —
 * same as `RHFSelect` itself. Omitting `multiple` from the props type would
 * pin `Multiple` to its default and make passing `multiple={false}` a type
 * error.
 *
 * A similar approach can be taken to create reusable styled components for:
 * - RHFNativeSelect
 * - RHFCheckboxGroup
 * - RHFRadioGroup
 */

import { type FieldValues } from 'react-hook-form';
import { Poppins } from 'next/font/google';
import RHFSelect, {
  type RHFSelectProps
} from '@nish1896/rhf-mui-components/mui/select';
import type { StrNumObjOption } from '@nish1896/rhf-mui-components/types';

const poppins = Poppins({
  subsets: ['latin'],
  style: 'italic',
  weight: ['100', '200', '300', '400', '500', '600', '700', '800', '900']
});

type StyledSelectProps<
  T extends FieldValues,
  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 = true
> = Omit<
  RHFSelectProps<T, Option, LabelKey, ValueKey, Multiple>,
  'showLabelAboveFormField'
>;

const StyledSelect = <
  T extends FieldValues,
  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 = true
>({
  formLabelProps,
  ...rest
}: StyledSelectProps<T, Option, LabelKey, ValueKey, Multiple>) => {

  return (
    <RHFSelect
      formLabelProps={{
        ...formLabelProps,
        sx: {
          fontFamily: poppins.style.fontFamily,
          fontWeight: 400,
          ...formLabelProps?.sx
        }
      }}
      showLabelAboveFormField
      {...rest}
    />
  );
};

export default StyledSelect;

/**
 * The below code snippet illustrates how to create a reusable styled Autocomplete
 * component using RHFAutocomplete, which can be used throughout the application.
 *
 * A similar approach can be taken to create reusable styled components for:
 * - RHFAutocompleteObject
 * - RHFMultiAutocomplete
 * - RHFMultiAutocompleteObject
 */

import { type FieldValues } from 'react-hook-form';
import RHFAutocomplete, {
  type RHFAutocompleteProps
} from '@nish1896/rhf-mui-components/mui/autocomplete';
import type { StrObjOption } from '@nish1896/rhf-mui-components/types';

type StyledAutocompleteProps<
  T extends FieldValues,
  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<RHFAutocompleteProps<T, Option, LabelKey, ValueKey, true, DisableClearable, FreeSolo>, 'multiple'>;

const StyledAutocomplete = <
  T extends FieldValues,
  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<T, Option, LabelKey, ValueKey, DisableClearable, FreeSolo>) => {
  return (
    <RHFAutocomplete
      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://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 FieldValues } from 'react-hook-form';
import type { Theme } from '@mui/material/styles';
import RHFSwitch, {
  type RHFSwitchProps
} from '@nish1896/rhf-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 = <T extends FieldValues>(sx: RHFSwitchProps<T>['sx']) =>
  /* eslint-disable-next-line no-nested-ternary */
  (Array.isArray(sx) ? sx : sx ? [sx] : []);

const StyledIOSSwitch = <T extends FieldValues>({
  sx,
  formControlLabelProps,
  ...rest
}: RHFSwitchProps<T>) => {
  const { sx: labelSx, ...otherLabelProps } = formControlLabelProps ?? {};
  return (
    <RHFSwitch
      {...rest}
      disableRipple
      focusVisibleClassName=".Mui-focusVisible"
      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 RHFDatePicker, 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 (`fieldName`, `control`, `onValueChange`…).
 * RHFDatePicker 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:
 * - RHFTimePicker
 * - RHFDateTimePicker
 */

import { type FieldValues } from 'react-hook-form';
import CalendarMonthIcon from '@mui/icons-material/CalendarMonth';
import {
  RHFDatePicker,
  type RHFDatePickerProps
} from '@nish1896/rhf-mui-components/mui-pickers/date';

const brandColor = '#007bff';

type StyledDatePickerProps<T extends FieldValues>
  = Omit<RHFDatePickerProps<T>, 'showLabelAboveFormField'>;

const StyledDatePicker = <T extends FieldValues>({
  slotProps,
  ...rest
}: StyledDatePickerProps<T>) => {
  return (
    <RHFDatePicker
      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;