Kombai Logo

How to Use React MUI Date and Time Pickers to Build a Custom Booking Form

Building a booking form is a common requirement in many web applications, from appointment schedulers to event registration systems. Material UI (MUI) offers powerful date and time picker components that can simplify this process significantly. In this article, I'll walk you through creating a comprehensive booking form using MUI's date and time pickers.

What You'll Learn

By the end of this tutorial, you'll be able to:

  • Implement MUI date and time pickers in a React application
  • Create a fully functional booking form with validation
  • Customize the appearance and behavior of the pickers
  • Handle form state and submission
  • Implement advanced features like date range selection and time constraints
  • Troubleshoot common issues that arise when working with date/time components

Understanding MUI Date and Time Pickers

Before diving into implementation, let's understand what MUI offers for date and time selection. MUI's date and time pickers are part of the MUI X package, which provides advanced components beyond the core MUI library. These components offer a rich set of features for selecting dates and times with a polished UI that follows Material Design principles.

The date and time pickers in MUI X come in several variants, including basic date pickers, time pickers, date-time pickers, and date range pickers. They support both mobile and desktop interfaces, various formatting options, and can be easily integrated with form libraries like Formik or React Hook Form.

Key Components Overview

MUI X provides several picker components that we'll use in our booking form:

  1. DatePicker - For selecting a single date
  2. TimePicker - For selecting a time
  3. DateTimePicker - For selecting both date and time together
  4. DateRangePicker - For selecting a range of dates
  5. StaticDatePicker - For embedding a calendar directly in the UI

Each of these components is highly customizable and can be configured to meet specific requirements.

Setting Up Your Project

Let's start by setting up a React project with MUI and the necessary date picker components.

Installation

First, we need to create a React application and install the required dependencies:

# Create a new React project
npx create-react-app mui-booking-form

# Navigate to the project directory
cd mui-booking-form

# Install MUI core
npm install @mui/material @emotion/react @emotion/styled

# Install MUI X Date Pickers and date library
npm install @mui/x-date-pickers dayjs

I'm using dayjs as the date library here, but MUI X Date Pickers also supports other libraries like moment, luxon, or date-fns. Choose the one you're most comfortable with.

Configuring Date Providers

Before we can use the date picker components, we need to set up the date provider. This provider makes the date library available to all the picker components in your application.

Let's modify our App.js file to include the necessary providers:

import React from 'react';
import { LocalizationProvider } from '@mui/x-date-pickers';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import BookingForm from './BookingForm';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';

// Create a theme instance
const theme = createTheme({
palette: {
  primary: {
    main: '#1976d2',
  },
  secondary: {
    main: '#dc004e',
  },
},
});

function App() {
return (
  <ThemeProvider theme={theme}>
    <CssBaseline />
    <LocalizationProvider dateAdapter={AdapterDayjs}>
      <div className="App">
        <header className="App-header">
          <h1>Booking System</h1>
        </header>
        <main>
          <BookingForm />
        </main>
      </div>
    </LocalizationProvider>
  </ThemeProvider>
);
}

export default App;

In this setup, we've:

  1. Wrapped our application with ThemeProvider to apply consistent styling
  2. Added LocalizationProvider with the DayJS adapter
  3. Included a placeholder for our BookingForm component that we'll create next

Deep Dive into MUI Date and Time Picker Components

Before building our booking form, let's explore the MUI Date and Time Picker components in detail to understand what they offer.

DatePicker Component

The DatePicker component allows users to select a date from a calendar interface. Here's a comprehensive look at its capabilities:

Essential Props

PropTypeDefaultDescription
valueDate | nullnullThe selected date value
onChangefunctionrequiredCallback fired when the value changes
disabledbooleanfalseIf true, the picker and text field are disabled
disableFuturebooleanfalseIf true, disables future dates
disablePastbooleanfalseIf true, disables past dates
minDateDate-The minimum selectable date
maxDateDate-The maximum selectable date
shouldDisableDatefunction() => falseCustom function to disable specific dates
viewsarray['year', 'day']Available views (year, month, day)
openTostring'day'The view to open with
formatstring'MM/DD/YYYY'Format string for the date

Controlled vs Uncontrolled Usage

The DatePicker can be used in either controlled or uncontrolled mode:

Controlled mode (recommended):

import { useState } from 'react';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import dayjs from 'dayjs';

function ControlledDatePicker() {
const [value, setValue] = useState(dayjs());

return (
  <DatePicker
    label="Controlled date"
    value={value}
    onChange={(newValue) => setValue(newValue)}
  />
);
}

Uncontrolled mode:

import { DatePicker } from '@mui/x-date-pickers/DatePicker';

function UncontrolledDatePicker() {
return (
  <DatePicker
    label="Uncontrolled date"
    defaultValue={dayjs()}
  />
);
}

Customization Options

The DatePicker can be customized in several ways:

  1. Styling with the sx prop:
<DatePicker
label="Custom styled date picker"
sx={{
  '& .MuiInputBase-root': {
    borderRadius: 2,
  },
  '& .MuiOutlinedInput-notchedOutline': {
    borderColor: 'primary.main',
  },
}}
/>
  1. Theme customization:
const theme = createTheme({
components: {
  MuiDatePicker: {
    styleOverrides: {
      root: {
        backgroundColor: '#f5f5f5',
      },
    },
  },
},
});
  1. Custom day rendering:
<DatePicker
label="Custom day rendering"
slots={{
  day: (props) => {
    const isSelected = 
      !props.outsideCurrentMonth && 
      props.day.toString() === props.selectedDay?.toString();
    
    return (
      <div
        {...props}
        style={{
          ...props.style,
          backgroundColor: isSelected ? 'lightblue' : 'transparent',
          borderRadius: '50%',
        }}
      >
        {props.day.getDate()}
      </div>
    );
  },
}}
/>

Accessibility Features

The DatePicker component comes with built-in accessibility features:

  • Keyboard navigation (arrow keys to navigate dates)
  • Screen reader support
  • ARIA attributes
  • Focus management

To enhance accessibility further, you can add:

<DatePicker
label="Appointment Date"
aria-label="Choose your appointment date"
slotProps={{
  textField: {
    helperText: 'MM/DD/YYYY format',
    InputProps: {
      'aria-describedby': 'date-picker-helper-text',
    },
  },
}}
/>

TimePicker Component

The TimePicker component allows users to select a time. Here's a detailed look:

Essential Props

PropTypeDefaultDescription
valueDate | nullnullThe selected time value
onChangefunctionrequiredCallback fired when the value changes
ampmbooleantrue (US locale)12h/24h view for hour selection clock
minutesStepnumber1Step over minutes
minTimeDate-Minimum allowed time
maxTimeDate-Maximum allowed time
shouldDisableTimefunction() => falseCustom function to disable specific times
viewsarray['hours', 'minutes']Available views
formatstring'hh:mm a'Format string for the time

Usage Example

import { useState } from 'react';
import { TimePicker } from '@mui/x-date-pickers/TimePicker';
import dayjs from 'dayjs';

function TimePickerExample() {
const [value, setValue] = useState(dayjs().hour(9).minute(0));

return (
  <TimePicker
    label="Appointment Time"
    value={value}
    onChange={(newValue) => setValue(newValue)}
    ampm={true}
    minutesStep={15}
  />
);
}

Customizing Time Constraints

You can limit available times using various props:

// Only allow business hours (9 AM - 5 PM)
<TimePicker
label="Business Hours Only"
value={value}
onChange={handleChange}
minTime={dayjs().hour(9).minute(0)}
maxTime={dayjs().hour(17).minute(0)}
/>

Or disable specific times:

// Disable lunch hour (12-1 PM)
<TimePicker
label="Excluding Lunch Hour"
value={value}
onChange={handleChange}
shouldDisableTime={(timeValue, view) => {
  if (view === 'hours' && timeValue.hour() === 12) {
    return true;
  }
  return false;
}}
/>

DateTimePicker Component

The DateTimePicker combines both date and time selection in one component:

import { useState } from 'react';
import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker';
import dayjs from 'dayjs';

function DateTimePickerExample() {
const [value, setValue] = useState(dayjs());

return (
  <DateTimePicker
    label="Appointment Date & Time"
    value={value}
    onChange={(newValue) => setValue(newValue)}
    disablePast
    minutesStep={15}
  />
);
}

This component inherits props from both DatePicker and TimePicker, giving you comprehensive control over both date and time selection.

DateRangePicker Component

For selecting a range of dates, MUI X provides the DateRangePicker:

import { useState } from 'react';
import { DateRangePicker } from '@mui/x-date-pickers-pro/DateRangePicker';
import dayjs from 'dayjs';

function DateRangePickerExample() {
const [value, setValue] = useState([dayjs(), dayjs().add(7, 'day')]);

return (
  <DateRangePicker
    label="Date Range"
    value={value}
    onChange={(newValue) => setValue(newValue)}
    disablePast
  />
);
}

Note: The DateRangePicker is part of the Pro package of MUI X, which requires a license for commercial use.

Building the Booking Form Step by Step

Now that we understand the components, let's build our booking form. We'll create a form that allows users to book appointments with date, time, and additional details.

Step 1: Create the Basic Form Structure

First, let's create a new file called BookingForm.js with the basic structure:

import React, { useState } from 'react';
import {
Box,
Paper,
Typography,
TextField,
Button,
Grid,
MenuItem,
Snackbar,
Alert,
} from '@mui/material';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { TimePicker } from '@mui/x-date-pickers/TimePicker';
import dayjs from 'dayjs';

const BookingForm = () => {
// Form state
const [formData, setFormData] = useState({
  name: '',
  email: '',
  phone: '',
  service: '',
  date: dayjs(),
  time: dayjs().hour(9).minute(0),
  notes: '',
});

// Form submission state
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitSuccess, setSubmitSuccess] = useState(false);
const [submitError, setSubmitError] = useState(false);

// Handle input changes
const handleChange = (e) => {
  const { name, value } = e.target;
  setFormData({
    ...formData,
    [name]: value,
  });
};

// Handle date change
const handleDateChange = (newDate) => {
  setFormData({
    ...formData,
    date: newDate,
  });
};

// Handle time change
const handleTimeChange = (newTime) => {
  setFormData({
    ...formData,
    time: newTime,
  });
};

// Handle form submission
const handleSubmit = (e) => {
  e.preventDefault();
  setIsSubmitting(true);
  
  // Simulate API call
  setTimeout(() => {
    console.log('Form submitted:', formData);
    setIsSubmitting(false);
    setSubmitSuccess(true);
    // Reset form after successful submission
    setFormData({
      name: '',
      email: '',
      phone: '',
      service: '',
      date: dayjs(),
      time: dayjs().hour(9).minute(0),
      notes: '',
    });
  }, 1500);
};

// Available services
const services = [
  'Consultation',
  'Haircut',
  'Massage',
  'Dental Checkup',
  'Fitness Training',
];

return (
  <Box sx={{ maxWidth: 800, mx: 'auto', p: 2 }}>
    <Paper elevation={3} sx={{ p: 4 }}>
      <Typography variant="h4" component="h1" gutterBottom>
        Book Your Appointment
      </Typography>
      <Typography variant="body1" sx={{ mb: 4 }}>
        Fill out the form below to schedule your appointment.
      </Typography>
      
      <form onSubmit={handleSubmit}>
        <Grid container spacing={3}>
          {/* Form fields will go here */}
        </Grid>
      </form>
    </Paper>
    
    {/* Success/Error messages */}
    <Snackbar 
      open={submitSuccess} 
      autoHideDuration={6000} 
      onClose={() => setSubmitSuccess(false)}
    >
      <Alert severity="success">Appointment booked successfully!</Alert>
    </Snackbar>
    
    <Snackbar 
      open={submitError} 
      autoHideDuration={6000} 
      onClose={() => setSubmitError(false)}
    >
      <Alert severity="error">There was an error booking your appointment. Please try again.</Alert>
    </Snackbar>
  </Box>
);
};

export default BookingForm;

This sets up our basic form structure with state management for the form fields and submission status. We've also included a simulated API call to demonstrate how you would handle form submission.

Step 2: Add Personal Information Fields

Let's add the personal information fields to our form:

{/* Personal Information */}
<Grid item xs={12}>
<Typography variant="h6" sx={{ mb: 2 }}>
  Personal Information
</Typography>
</Grid>

<Grid item xs={12} sm={6}>
<TextField
  fullWidth
  required
  id="name"
  name="name"
  label="Full Name"
  value={formData.name}
  onChange={handleChange}
  variant="outlined"
/>
</Grid>

<Grid item xs={12} sm={6}>
<TextField
  fullWidth
  required
  id="email"
  name="email"
  label="Email Address"
  type="email"
  value={formData.email}
  onChange={handleChange}
  variant="outlined"
/>
</Grid>

<Grid item xs={12} sm={6}>
<TextField
  fullWidth
  id="phone"
  name="phone"
  label="Phone Number"
  value={formData.phone}
  onChange={handleChange}
  variant="outlined"
/>
</Grid>

<Grid item xs={12} sm={6}>
<TextField
  fullWidth
  required
  id="service"
  name="service"
  select
  label="Service"
  value={formData.service}
  onChange={handleChange}
  variant="outlined"
>
  {services.map((service) => (
    <MenuItem key={service} value={service}>
      {service}
    </MenuItem>
  ))}
</TextField>
</Grid>

These fields collect the user's basic information and the service they're interested in booking.

Step 3: Implement Date and Time Selection

Now, let's add the date and time pickers to our form:

{/* Date and Time Selection */}
<Grid item xs={12}>
<Typography variant="h6" sx={{ mt: 2, mb: 2 }}>
  Date and Time
</Typography>
</Grid>

<Grid item xs={12} sm={6}>
<DatePicker
  label="Appointment Date"
  value={formData.date}
  onChange={handleDateChange}
  disablePast
  sx={{ width: '100%' }}
  slotProps={{
    textField: {
      required: true,
      fullWidth: true,
    },
  }}
/>
</Grid>

<Grid item xs={12} sm={6}>
<TimePicker
  label="Appointment Time"
  value={formData.time}
  onChange={handleTimeChange}
  minutesStep={15}
  sx={{ width: '100%' }}
  slotProps={{
    textField: {
      required: true,
      fullWidth: true,
    },
  }}
/>
</Grid>

Here, we've added:

  1. A DatePicker that only allows future dates
  2. A TimePicker with 15-minute intervals for easier scheduling

Step 4: Add Notes and Submit Button

Let's complete our form with a notes field and submit button:

{/* Additional Notes */}
<Grid item xs={12}>
<TextField
  fullWidth
  id="notes"
  name="notes"
  label="Additional Notes"
  multiline
  rows={4}
  value={formData.notes}
  onChange={handleChange}
  variant="outlined"
/>
</Grid>

{/* Submit Button */}
<Grid item xs={12}>
<Button
  type="submit"
  variant="contained"
  size="large"
  disabled={isSubmitting}
  sx={{ mt: 2 }}
>
  {isSubmitting ? 'Booking...' : 'Book Appointment'}
</Button>
</Grid>

Step 5: Add Validation and Business Rules

Let's enhance our form with validation and business rules to ensure we collect valid data:

import React, { useState, useEffect } from 'react';
// ... other imports

const BookingForm = () => {
// ... existing state

// Add validation state
const [errors, setErrors] = useState({});

// Business hours
const businessHours = {
  start: 9, // 9 AM
  end: 17,  // 5 PM
};

// Validate time is within business hours
const validateTime = (time) => {
  const hour = time.hour();
  return hour >= businessHours.start && hour < businessHours.end;
};

// Validate form on change
useEffect(() => {
  const newErrors = {};
  
  // Email validation
  if (formData.email && !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+.[A-Z]{2,4}$/i.test(formData.email)) {
    newErrors.email = 'Invalid email address';
  }
  
  // Phone validation (optional field)
  if (formData.phone && !/^[0-9]{10}$/.test(formData.phone.replace(/[^0-9]/g, ''))) {
    newErrors.phone = 'Phone should be 10 digits';
  }
  
  // Time validation
  if (formData.time && !validateTime(formData.time)) {
    newErrors.time = `Appointments are only available between ${businessHours.start}AM and ${businessHours.end}PM`;
  }
  
  setErrors(newErrors);
}, [formData]);

// Check if form is valid
const isFormValid = () => {
  // Required fields
  if (!formData.name || !formData.email || !formData.service || !formData.date || !formData.time) {
    return false;
  }
  
  // No validation errors
  return Object.keys(errors).length === 0;
};

// Updated handleSubmit
const handleSubmit = (e) => {
  e.preventDefault();
  
  if (!isFormValid()) {
    setSubmitError(true);
    return;
  }
  
  setIsSubmitting(true);
  
  // Simulate API call
  setTimeout(() => {
    console.log('Form submitted:', formData);
    setIsSubmitting(false);
    setSubmitSuccess(true);
    // Reset form after successful submission
    setFormData({
      name: '',
      email: '',
      phone: '',
      service: '',
      date: dayjs(),
      time: dayjs().hour(9).minute(0),
      notes: '',
    });
  }, 1500);
};

// ... rest of the component
}

Now, let's update our form fields to display validation errors:

<Grid item xs={12} sm={6}>
<TextField
  fullWidth
  required
  id="email"
  name="email"
  label="Email Address"
  type="email"
  value={formData.email}
  onChange={handleChange}
  variant="outlined"
  error={!!errors.email}
  helperText={errors.email}
/>
</Grid>

<Grid item xs={12} sm={6}>
<TextField
  fullWidth
  id="phone"
  name="phone"
  label="Phone Number"
  value={formData.phone}
  onChange={handleChange}
  variant="outlined"
  error={!!errors.phone}
  helperText={errors.phone}
/>
</Grid>

{/* ... */}

<Grid item xs={12} sm={6}>
<TimePicker
  label="Appointment Time"
  value={formData.time}
  onChange={handleTimeChange}
  minutesStep={15}
  sx={{ width: '100%' }}
  slotProps={{
    textField: {
      required: true,
      fullWidth: true,
      error: !!errors.time,
      helperText: errors.time,
    },
  }}
  shouldDisableTime={(timeValue, view) => {
    if (view === 'hours' && (timeValue.hour() < businessHours.start || timeValue.hour() >= businessHours.end)) {
      return true;
    }
    return false;
  }}
/>
</Grid>

Step 6: Implement Business Rules for Date Selection

Let's add more sophisticated business rules for date selection, such as disabling weekends or specific dates:

// Disable weekends and holidays
const isWeekend = (date) => {
const day = date.day();
return day === 0 || day === 6; // 0 is Sunday, 6 is Saturday
};

// Example holidays (you would typically get these from a database or API)
const holidays = [
dayjs('2023-12-25'), // Christmas
dayjs('2023-12-31'), // New Year's Eve
dayjs('2024-01-01'), // New Year's Day
];

const isHoliday = (date) => {
return holidays.some(holiday => 
  holiday.format('YYYY-MM-DD') === date.format('YYYY-MM-DD')
);
};

// Check if a date should be disabled
const shouldDisableDate = (date) => {
return isWeekend(date) || isHoliday(date);
};

Now, update the DatePicker:

<DatePicker
label="Appointment Date"
value={formData.date}
onChange={handleDateChange}
disablePast
shouldDisableDate={shouldDisableDate}
sx={{ width: '100%' }}
slotProps={{
  textField: {
    required: true,
    fullWidth: true,
  },
}}
/>

Step 7: Add Date and Time Availability Checking

In a real booking system, you'd want to check if a specific date and time slot is available. Let's simulate this:

// Simulated booked slots (in a real app, this would come from your backend)
const bookedSlots = [
{ date: '2023-11-15', time: '10:00' },
{ date: '2023-11-15', time: '11:00' },
{ date: '2023-11-16', time: '14:00' },
{ date: '2023-11-17', time: '09:00' },
];

// Check if a time slot is available
const isTimeSlotAvailable = (date, time) => {
const dateStr = date.format('YYYY-MM-DD');
const timeStr = time.format('HH:mm');

return !bookedSlots.some(
  slot => slot.date === dateStr && slot.time === timeStr
);
};

// Add a function to show available slots
const getAvailableTimeSlots = (date) => {
const slots = [];
const dateStr = date.format('YYYY-MM-DD');

// Generate all possible slots
for (let hour = businessHours.start; hour < businessHours.end; hour++) {
  for (let minute = 0; minute < 60; minute += 15) {
    const timeStr = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
    
    // Check if slot is available
    const isAvailable = !bookedSlots.some(
      slot => slot.date === dateStr && slot.time === timeStr
    );
    
    if (isAvailable) {
      slots.push({
        hour,
        minute,
        label: dayjs().hour(hour).minute(minute).format('h:mm A')
      });
    }
  }
}

return slots;
};

Now, let's modify our form to show available time slots based on the selected date:

// Add state for available time slots
const [availableTimeSlots, setAvailableTimeSlots] = useState([]);

// Update available time slots when date changes
useEffect(() => {
if (formData.date) {
  setAvailableTimeSlots(getAvailableTimeSlots(formData.date));
}
}, [formData.date]);

// In the render function, replace the TimePicker with:
<Grid item xs={12} sm={6}>
<TextField
  select
  fullWidth
  required
  id="timeSlot"
  name="timeSlot"
  label="Available Time Slots"
  value={formData.time ? formData.time.format('HH:mm') : ''}
  onChange={(e) => {
    const [hour, minute] = e.target.value.split(':').map(Number);
    setFormData({
      ...formData,
      time: dayjs().hour(hour).minute(minute)
    });
  }}
  variant="outlined"
  error={!!errors.time}
  helperText={errors.time || 'Select an available time slot'}
>
  {availableTimeSlots.length > 0 ? (
    availableTimeSlots.map((slot) => (
      <MenuItem 
        key={`${slot.hour}:${slot.minute}`} 
        value={`${slot.hour.toString().padStart(2, '0')}:${slot.minute.toString().padStart(2, '0')}`}
      >
        {slot.label}
      </MenuItem>
    ))
  ) : (
    <MenuItem disabled value="">
      No available slots for this date
    </MenuItem>
  )}
</TextField>
</Grid>

Step 8: Add a Date Range Option for Multi-Day Bookings

For services that might span multiple days, let's add a date range option:

// Add to state
const [isMultiDay, setIsMultiDay] = useState(false);
const [dateRange, setDateRange] = useState([dayjs(), dayjs().add(1, 'day')]);

// Add UI for toggling multi-day booking
<Grid item xs={12}>
<FormControlLabel
  control={
    <Switch
      checked={isMultiDay}
      onChange={(e) => setIsMultiDay(e.target.checked)}
      name="multiDay"
      color="primary"
    />
  }
  label="Multi-day booking"
/>
</Grid>

{/* Conditional rendering based on booking type */}
{isMultiDay ? (
<Grid item xs={12}>
  <DateRangePicker
    startText="Check-in"
    endText="Check-out"
    value={dateRange}
    onChange={(newValue) => {
      setDateRange(newValue);
    }}
    disablePast
    shouldDisableDate={shouldDisableDate}
    sx={{ width: '100%' }}
  />
</Grid>
) : (
<Grid item xs={12} sm={6}>
  <DatePicker
    label="Appointment Date"
    value={formData.date}
    onChange={handleDateChange}
    disablePast
    shouldDisableDate={shouldDisableDate}
    sx={{ width: '100%' }}
    slotProps={{
      textField: {
        required: true,
        fullWidth: true,
      },
    }}
  />
</Grid>
)}

Note: Make sure to import the necessary components:

import { DateRangePicker } from '@mui/x-date-pickers-pro/DateRangePicker';
import { FormControlLabel, Switch } from '@mui/material';

Step 9: Add Visual Feedback and Summary

Let's enhance our form with visual feedback and a booking summary:

// Add state for showing summary
const [showSummary, setShowSummary] = useState(false);

// Add to the form, before the submit button
<Grid item xs={12}>
<Paper 
  elevation={1} 
  sx={{ 
    p: 2, 
    mt: 2, 
    mb: 2, 
    bgcolor: 'primary.light', 
    color: 'primary.contrastText',
    display: showSummary ? 'block' : 'none'
  }}
>
  <Typography variant="h6" gutterBottom>
    Booking Summary
  </Typography>
  <Typography variant="body1">
    <strong>Name:</strong> {formData.name}
  </Typography>
  <Typography variant="body1">
    <strong>Service:</strong> {formData.service}
  </Typography>
  <Typography variant="body1">
    <strong>Date:</strong> {formData.date.format('dddd, MMMM D, YYYY')}
  </Typography>
  <Typography variant="body1">
    <strong>Time:</strong> {formData.time.format('h:mm A')}
  </Typography>
</Paper>
</Grid>

{/* Update the submit button section */}
<Grid item xs={12} container spacing={2} justifyContent="space-between">
<Grid item>
  <Button
    variant="outlined"
    onClick={() => setShowSummary(!showSummary)}
  >
    {showSummary ? 'Hide Summary' : 'Preview Booking'}
  </Button>
</Grid>
<Grid item>
  <Button
    type="submit"
    variant="contained"
    size="large"
    disabled={isSubmitting || !isFormValid()}
    sx={{ mt: 2 }}
  >
    {isSubmitting ? 'Booking...' : 'Book Appointment'}
  </Button>
</Grid>
</Grid>

Advanced Capabilities and Customizations

Now that we have a functional booking form, let's explore some advanced capabilities and customizations for MUI date and time pickers.

Custom Date Formats

You can customize the date format displayed in the input field:

<DatePicker
label="Custom Format Date"
value={formData.date}
onChange={handleDateChange}
format="MMMM D, YYYY" // Shows as "November 15, 2023"
sx={{ width: '100%' }}
slotProps={{
  textField: {
    fullWidth: true,
  },
}}
/>

Custom Styling with Theme Overrides

You can customize the appearance of the date picker using theme overrides:

const theme = createTheme({
components: {
  MuiPickersDay: {
    styleOverrides: {
      root: {
        fontSize: '0.9rem',
        '&.Mui-selected': {
          backgroundColor: '#2e7d32',
          '&:hover': {
            backgroundColor: '#1b5e20',
          },
        },
      },
      today: {
        border: '1px solid #2e7d32',
      },
    },
  },
  MuiClock: {
    styleOverrides: {
      pin: {
        backgroundColor: '#2e7d32',
      },
      clock: {
        backgroundColor: '#f1f8e9',
      },
    },
  },
},
});

Inline Calendar Display

For a more user-friendly experience, you can display the calendar inline:

import { StaticDatePicker } from '@mui/x-date-pickers/StaticDatePicker';

// In your component
<Grid item xs={12}>
<Paper elevation={1} sx={{ p: 2 }}>
  <Typography variant="h6" gutterBottom>
    Select Date
  </Typography>
  <StaticDatePicker
    displayStaticWrapperAs="desktop"
    value={formData.date}
    onChange={handleDateChange}
    disablePast
    shouldDisableDate={shouldDisableDate}
    renderInput={(params) => <TextField {...params} />}
  />
</Paper>
</Grid>

Custom Day Rendering

You can customize how days are rendered in the calendar:

<DatePicker
label="Appointment Date"
value={formData.date}
onChange={handleDateChange}
disablePast
shouldDisableDate={shouldDisableDate}
slots={{
  day: (props) => {
    // Add price or availability information to specific dates
    const date = props.day;
    const dateStr = date.format('YYYY-MM-DD');
    
    // Example: Add pricing information to dates
    const prices = {
      '2023-11-20': '$50',
      '2023-11-21': '$45',
      '2023-11-22': '$60',
    };
    
    const price = prices[dateStr];
    
    return (
      <div
        {...props}
        style={{
          ...props.style,
          position: 'relative',
          height: '40px',
        }}
      >
        <span>{date.date()}</span>
        {price && (
          <span style={{
            position: 'absolute',
            bottom: 2,
            left: 0,
            right: 0,
            fontSize: '0.6rem',
            textAlign: 'center',
            color: 'green',
          }}>
            {price}
          </span>
        )}
      </div>
    );
  },
}}
sx={{ width: '100%' }}
slotProps={{
  textField: {
    required: true,
    fullWidth: true,
  },
}}
/>

Integration with Form Libraries

MUI date pickers can be easily integrated with form libraries like Formik or React Hook Form. Here's an example with Formik:

import { useFormik } from 'formik';
import * as Yup from 'yup';

// Validation schema
const validationSchema = Yup.object({
name: Yup.string().required('Name is required'),
email: Yup.string().email('Invalid email address').required('Email is required'),
service: Yup.string().required('Service is required'),
date: Yup.date().required('Date is required'),
time: Yup.date().required('Time is required'),
});

// In your component
const formik = useFormik({
initialValues: {
  name: '',
  email: '',
  phone: '',
  service: '',
  date: dayjs(),
  time: dayjs().hour(9).minute(0),
  notes: '',
},
validationSchema,
onSubmit: (values) => {
  console.log('Form submitted:', values);
  // Handle form submission
},
});

// In your JSX
<form onSubmit={formik.handleSubmit}>
{/* ... other fields ... */}

<Grid item xs={12} sm={6}>
  <DatePicker
    label="Appointment Date"
    value={formik.values.date}
    onChange={(newValue) => {
      formik.setFieldValue('date', newValue);
    }}
    disablePast
    shouldDisableDate={shouldDisableDate}
    sx={{ width: '100%' }}
    slotProps={{
      textField: {
        required: true,
        fullWidth: true,
        error: formik.touched.date && Boolean(formik.errors.date),
        helperText: formik.touched.date && formik.errors.date,
      },
    }}
  />
</Grid>

{/* ... other fields ... */}
</form>

Best Practices and Common Issues

When working with MUI date and time pickers in a booking form, here are some best practices and common issues to be aware of:

Best Practices

  1. Always provide clear labels and helper text

Ensure your date and time pickers have clear labels and, when necessary, helper text to guide users:

<DatePicker
label="Appointment Date"
slotProps={{
  textField: {
    helperText: "Select a weekday for your appointment",
  },
}}
// other props
/>
  1. Handle timezones appropriately

When working with dates and times, always consider timezone issues:

// Store timezone information with the date
const handleSubmit = (e) => {
e.preventDefault();

// Get user's timezone
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;

// Include timezone in submission data
const submissionData = {
  ...formData,
  timezone,
  // Convert to ISO string for backend storage
  dateTimeISO: dayjs(formData.date)
    .hour(formData.time.hour())
    .minute(formData.time.minute())
    .toISOString(),
};

// Submit data
console.log('Submitting with timezone info:', submissionData);
};
  1. Provide reasonable defaults

Always set reasonable default values for your date and time pickers:

// Set default to next business day at 9 AM
const getNextBusinessDay = () => {
let date = dayjs().add(1, 'day');

// Skip to Monday if it's Friday or weekend
const day = date.day();
if (day === 5) { // Friday
  date = date.add(3, 'day');
} else if (day === 6) { // Saturday
  date = date.add(2, 'day');
} else if (day === 0) { // Sunday
  date = date.add(1, 'day');
}

return date;
};

// Use in initial state
const [formData, setFormData] = useState({
// other fields...
date: getNextBusinessDay(),
time: dayjs().hour(9).minute(0),
});
  1. Optimize for mobile

Ensure your date and time pickers work well on mobile devices:

<DatePicker
label="Appointment Date"
value={formData.date}
onChange={handleDateChange}
disablePast
// Optimize for mobile
desktopModeMediaQuery="@media (min-width: 768px)"
// other props
/>

Common Issues and Fixes

  1. Date formatting inconsistencies

Problem: Different locales display dates differently, causing confusion.

Solution: Explicitly set the format and use the LocalizationProvider with the appropriate locale:

import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import 'dayjs/locale/fr'; // Import the locale you need

// In your App.js or component
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="fr">
{/* Your components */}
</LocalizationProvider>
  1. Time picker showing 24-hour format when 12-hour is expected

Problem: Time format doesn't match user expectations.

Solution: Explicitly set the time format:

<TimePicker
label="Appointment Time"
value={formData.time}
onChange={handleTimeChange}
ampm={true} // Use 12-hour format with AM/PM
// other props
/>
  1. Date validation errors

Problem: Users can still select invalid dates despite validation.

Solution: Combine built-in validation with custom validation:

// Combine multiple validation rules
const shouldDisableDate = (date) => {
// Check if weekend
if (isWeekend(date)) return true;

// Check if holiday
if (isHoliday(date)) return true;

// Check if fully booked
if (isFullyBooked(date)) return true;

return false;
};

// Use in DatePicker
<DatePicker
shouldDisableDate={shouldDisableDate}
// other props
/>
  1. Performance issues with many disabled dates

Problem: Calculating many disabled dates can cause performance issues.

Solution: Optimize your validation functions:

// Use memoization for expensive calculations
import { useMemo } from 'react';

// In your component
const bookedDates = useMemo(() => {
// This calculation only runs when dependencies change
return calculateBookedDates(availabilityData);
}, [availabilityData]);

// Use a more efficient check
const shouldDisableDate = useCallback((date) => {
const dateStr = date.format('YYYY-MM-DD');
return bookedDates.has(dateStr); // Using a Set for O(1) lookup
}, [bookedDates]);
  1. Form submission with incorrect date/time format

Problem: Dates are submitted in an unexpected format.

Solution: Format dates consistently before submission:

const handleSubmit = (e) => {
e.preventDefault();

// Create a consistent date-time object
const appointmentDateTime = dayjs(formData.date)
  .hour(formData.time.hour())
  .minute(formData.time.minute())
  .second(0);

const formattedData = {
  ...formData,
  // Format for display
  formattedDateTime: appointmentDateTime.format('YYYY-MM-DD HH:mm'),
  // ISO string for API
  isoDateTime: appointmentDateTime.toISOString(),
};

console.log('Submitting:', formattedData);
// Submit to API
};

Wrapping Up

In this comprehensive guide, we've explored how to build a fully functional booking form using MUI's date and time picker components. We've covered everything from basic setup to advanced customizations, validation, and integration with form libraries.

The MUI date and time pickers offer powerful functionality out of the box while remaining highly customizable. By following the patterns and practices outlined in this article, you can create user-friendly booking experiences that handle complex business rules and validation requirements.

Remember to always consider the user experience when implementing date and time selection interfaces. Clear labels, helpful guidance, and intuitive defaults go a long way toward creating a smooth booking process. Additionally, proper validation and error handling ensure that users can't submit invalid bookings, saving time and frustration for both users and administrators.