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:
DatePicker
- For selecting a single dateTimePicker
- For selecting a timeDateTimePicker
- For selecting both date and time togetherDateRangePicker
- For selecting a range of datesStaticDatePicker
- 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:
- Wrapped our application with
ThemeProvider
to apply consistent styling - Added
LocalizationProvider
with the DayJS adapter - 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
Prop | Type | Default | Description |
---|---|---|---|
value | Date | null | null | The selected date value |
onChange | function | required | Callback fired when the value changes |
disabled | boolean | false | If true, the picker and text field are disabled |
disableFuture | boolean | false | If true, disables future dates |
disablePast | boolean | false | If true, disables past dates |
minDate | Date | - | The minimum selectable date |
maxDate | Date | - | The maximum selectable date |
shouldDisableDate | function | () => false | Custom function to disable specific dates |
views | array | ['year', 'day'] | Available views (year, month, day) |
openTo | string | 'day' | The view to open with |
format | string | '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:
- Styling with the
sx
prop:
<DatePicker
label="Custom styled date picker"
sx={{
'& .MuiInputBase-root': {
borderRadius: 2,
},
'& .MuiOutlinedInput-notchedOutline': {
borderColor: 'primary.main',
},
}}
/>
- Theme customization:
const theme = createTheme({
components: {
MuiDatePicker: {
styleOverrides: {
root: {
backgroundColor: '#f5f5f5',
},
},
},
},
});
- 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
Prop | Type | Default | Description |
---|---|---|---|
value | Date | null | null | The selected time value |
onChange | function | required | Callback fired when the value changes |
ampm | boolean | true (US locale) | 12h/24h view for hour selection clock |
minutesStep | number | 1 | Step over minutes |
minTime | Date | - | Minimum allowed time |
maxTime | Date | - | Maximum allowed time |
shouldDisableTime | function | () => false | Custom function to disable specific times |
views | array | ['hours', 'minutes'] | Available views |
format | string | '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:
- A DatePicker that only allows future dates
- 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
- 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
/>
- 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);
};
- 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),
});
- 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
- 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>
- 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
/>
- 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
/>
- 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]);
- 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.