Editor’s note: This post was last updated by Carlos Mucuho on 21 March 2024 to add advanced features to our custom React datepicker, including a time picker, the ability to block certain dates from the calendar, and more.
It’s quite common to see forms on the web that include one or more date fields. Whether it’s a person’s date of birth or a flight schedule, you want to ensure that the user supplies a valid date. A datepicker is a helpful addition to a form that displays a calendar along with a date input field, making it easy for users to schedule events or fill out registration forms.
HTML5 introduced a date
input type to improve date values in forms. The date
input type presents a datepicker to the user, but the appearance of this datepicker is inconsistent across browsers:
Most major browsers devalue the date
input type to a text
input for the user, causing issues both for the rendering of the UI and the formatting of data. When a user enters a text-based input, formatting issues can occur with the date. For example, with various date formats used globally, a lack of instructions for the user can cause difficulty when processing the data, resulting in a browser error message.
In this article, we’ll solve the issue by building a custom React datepicker from scratch using native JavaScript Date
objects. Here’s what our custom datepicker will look like:
To follow along with this tutorial, you’ll need the following:
Note: If you’re using npm ≥ v5.2, then you don’t need to install Create React App as a global dependency . Instead, you can use the npx command.
If you get stuck during this tutorial, please reference the GitHub repository. Let’s get started!
To create a new React application, run the command below, which acts as a quick start for you to create the custom React datepicker:
npx create-react-app react-custom-datepicker cd react-custom-datepicker
Alternatively, if you’re using Yarn, you can run the code below:
yarn create react-app react-custom-datepicker
npx is available in npm v≥5.2, which you can use to install Create React App globally on your machine. If you’ve installed CRA using the command below, you should uninstall it to ensure that npx is using the latest version:
npm install -g create-react-app
To uninstall the package, you can use either of the following commands:
//npm npm uninstall -g create-react-app //yarn yarn global remove create-react-app
To install the required dependencies via Yarn, run the following command:
yarn add [email protected] reactstrap styled-components prop-types
And to install dependencies via npm, run the following command:
npm install [email protected] reactstrap styled-components
To provide default styling, we’ve installed the Bootstrap package as a dependency. To include the Bootstrap v5 styles, edit the src/index.js
file and add the following line before every other import statement:
import "bootstrap/dist/css/bootstrap.min.css";
Our application requires the following components:
calendar
: Renders the custom calendar, providing functionality for date selectiondatepicker
: Renders a date
input and presents the calendar for the user to select the dateWe’ll store each component in its own directory with two files, index.js
and styles.js
. The index.js
file exports the component, while the styles.js
file exports styled-components, which is required by the component to add styling.
To create the component directories and files, run the following commands from your project’s root directory:
# Create directories mkdir -p src/components/Calendar src/components/Datepicker # Create files touch src/components/Calendar/index.js src/components/Calendar/styles.js touch src/components/Datepicker/index.js src/components/Datepicker/styles.js
Our application doesn’t require any external dependencies to handle dates. Instead, we’ll use helper functions. To create a file for the calendar helpers
module, run the following commands:
mkdir -p src/helpers touch src/helpers/calendar.js
Start the application by running either of the following commands in your terminal:
//yarn yarn start //npm npm start
Now, we can begin development. You’ll notice that a new browser tab has opened for you with live reloading functionality, keeping in sync with changes in the application.
helpers
moduleIn the calendar helpers
module we created earlier, we’ll define some helpers and constants, which we’ll export as named exports. Add the following code to the src/helpers/calendar.js
file:
// (int) The current year export const THIS_YEAR = +(new Date().getFullYear()); // (int) The current month starting from 1 - 12 // 1 => January, 12 => December export const THIS_MONTH = +(new Date().getMonth()) + 1; // Week days names and shortnames export const WEEK_DAYS = { Sunday: "Sun", Monday: "Mon", Tuesday: "Tue", Wednesday: "Wed", Thursday: "Thu", Friday: "Fri", Saturday: "Sat" } // Calendar months names and short names export const CALENDAR_MONTHS = { January: "Jan", February: "Feb", March: "Mar", April: "Apr", May: "May", June: "Jun", July: "Jul", August: "Aug", September: "Sep", October: "Oct", November: "Nov", December: "Dec" } // Weeks displayed on calendar export const CALENDAR_WEEKS = 6; // Pads a string value with leading zeroes(0) until length is reached // For example: zeroPad(5, 2) => "05" export const zeroPad = (value, length) => { return `${value}`.padStart(length, '0'); } // (int) Number days in a month for a given year from 28 - 31 export const getMonthDays = (month = THIS_MONTH, year = THIS_YEAR) => { const months30 = [4, 6, 9, 11]; const leapYear = year % 4 === 0; return month === 2 ? leapYear ? 29 : 28 : months30.includes(month) ? 30 : 31; } // (int) First day of the month for a given year from 1 - 7 // 1 => Sunday, 7 => Saturday export const getMonthFirstDay = (month = THIS_MONTH, year = THIS_YEAR) => { return +(new Date(`${year}-${zeroPad(month, 2)}-01`).getDay()) + 1; }
Methods like getDay()
and getMonth()
in Date.prototype
usually return a zero-based value. Therefore, as the first month of the year, January is 0
, while December is 11
. The first day of the week, Sunday, is 0
, while Saturday is 7
.
In the code snippet above, you’ll see that 1
was added to these zero-based values. Sunday becomes 1
for weekdays, and December becomes 12
for the month.
Lastly, notice that CALENDAR_WEEKS
is set to 6
; because a month typically spans four weeks, doing so allows the calendar to accommodate the last week of the previous month and the first week of the next month.
Let’s improve our calendar
component by adding additional helper functions. First, we’ll verify the formatting of the date in the calendar
module by appending the following code to the src/helpers/calendar.js
file:
// (bool) Checks if a value is a date - this is just a simple check export const isDate = date => { const isDate = Object.prototype.toString.call(date) === '[object Date]'; const isValidDate = date && !Number.isNaN(date.valueOf()); return isDate && isValidDate; } // (bool) Checks if two date values are of the same month and year export const isSameMonth = (date, basedate = new Date()) => { if (!(isDate(date) && isDate(basedate))) return false; const basedateMonth = +(basedate.getMonth()) + 1; const basedateYear = basedate.getFullYear(); const dateMonth = +(date.getMonth()) + 1; const dateYear = date.getFullYear(); return (+basedateMonth === +dateMonth) && (+basedateYear === +dateYear); } // (bool) Checks if two date values are the same day export const isSameDay = (date, basedate = new Date()) => { if (!(isDate(date) && isDate(basedate))) return false; const basedateDate = basedate.getDate(); const basedateMonth = +(basedate.getMonth()) + 1; const basedateYear = basedate.getFullYear(); const dateDate = date.getDate(); const dateMonth = +(date.getMonth()) + 1; const dateYear = date.getFullYear(); return (+basedateDate === +dateDate) && (+basedateMonth === +dateMonth) && (+basedateYear === +dateYear); } // (string) Formats the given date as YYYY-MM-DD // Months and Days are zero padded export const getDateISO = (date = new Date) => { if (!isDate(date)) return null; return [ date.getFullYear(), zeroPad(+date.getMonth() + 1, 2), zeroPad(+date.getDate(), 2) ].join('-'); } // ({month, year}) Gets the month and year before the given month and year // For example: getPreviousMonth(1, 2000) => {month: 12, year: 1999} // while: getPreviousMonth(12, 2000) => {month: 11, year: 2000} export const getPreviousMonth = (month, year) => { const prevMonth = (month > 1) ? month - 1 : 12; const prevMonthYear = (month > 1) ? year : year - 1; return { month: prevMonth, year: prevMonthYear }; } // ({month, year}) Gets the month and year after the given month and year // For example: getNextMonth(1, 2000) => {month: 2, year: 2000} // while: getNextMonth(12, 2000) => {month: 1, year: 2001} export const getNextMonth = (month, year) => { const nextMonth = (month < 12) ? month + 1 : 1; const nextMonthYear = (month < 12) ? year : year + 1; return { month: nextMonth, year: nextMonthYear }; }
Next, we’ll add the calendar builder function, which is the default export of the calendar helpers
module. The calendar builder function takes a month and year as arguments, then returns an array of 42 elements, each representing a calendar date in the format [YYYY, MM,DD]
.
Append the code snippet below to the src/helpers/calendar.js
file:
// Calendar builder for a month in the specified year // Returns an array of the calendar dates. // Each calendar date is represented as an array => [YYYY, MM, DD] export default (month = THIS_MONTH, year = THIS_YEAR) => { // Get number of days in the month and the month's first day const monthDays = getMonthDays(month, year); const monthFirstDay = getMonthFirstDay(month, year); // Get number of days to be displayed from previous and next months // These ensure a total of 42 days (6 weeks) displayed on the calendar const daysFromPrevMonth = monthFirstDay - 1; const daysFromNextMonth = (CALENDAR_WEEKS * 7) - (daysFromPrevMonth + monthDays); // Get the previous and next months and years const { month: prevMonth, year: prevMonthYear } = getPreviousMonth(month, year); const { month: nextMonth, year: nextMonthYear } = getNextMonth(month, year); // Get number of days in previous month const prevMonthDays = getMonthDays(prevMonth, prevMonthYear); // Builds dates to be displayed from previous month const prevMonthDates = [...new Array(daysFromPrevMonth)].map((n, index) => { const day = index + 1 + (prevMonthDays - daysFromPrevMonth); return [ prevMonthYear, zeroPad(prevMonth, 2), zeroPad(day, 2) ]; }); // Builds dates to be displayed from current month const thisMonthDates = [...new Array(monthDays)].map((n, index) => { const day = index + 1; return [year, zeroPad(month, 2), zeroPad(day, 2)]; }); // Builds dates to be displayed from next month const nextMonthDates = [...new Array(daysFromNextMonth)].map((n, index) => { const day = index + 1; return [nextMonthYear, zeroPad(nextMonth, 2), zeroPad(day, 2)]; }); // Combines all dates from previous, current and next months return [ ...prevMonthDates, ...thisMonthDates, ...nextMonthDates ]; }
The calendar dates returned span from the date of the previous month’s final week to the following month’s first week.
calendar
componentNow that we have the calendar helpers
module, let’s build our React calendar
component. Add the following code snippet to the src/components/Calendar/index.js
file:
import React, { Component, Fragment, useEffect, useState } from "react"; import PropTypes from "prop-types"; import * as Styled from "./styles"; import calendar, { isDate, isSameDay, isSameMonth, getDateISO, getNextMonth, getPreviousMonth, WEEK_DAYS, CALENDAR_MONTHS, } from "../../helpers/calendar"; export default function Calendar({ date, onDateChanged }) { const [dateState, setDateState] = useState({ current: new Date(), month: 0, year: 0, }); const [today, setToday] = useState(new Date()); useEffect(() => { addDateToState(date); }, []); const addDateToState = (date) => { const isDateObject = isDate(date); const _date = isDateObject ? date : new Date(); setDateState({ current: isDateObject ? date : null, month: +_date.getMonth() + 1, year: _date.getFullYear(), }); }; const getCalendarDates = () => { const { current, month, year } = dateState; console.log(typeof current); console.log(current); const calendarMonth = month || +current?.getMonth() + 1; const calendarYear = year || current?.getFullYear(); return calendar(calendarMonth, calendarYear); }; return ( <Styled.CalendarContainer> {renderMonthAndYear()} <Styled.CalendarGrid> <Fragment>{Object.keys(WEEK_DAYS).map(renderDayLabel)}</Fragment> <Fragment>{getCalendarDates().map(renderCalendarDate)}</Fragment> </Styled.CalendarGrid> </Styled.CalendarContainer> ); } Calendar.propTypes = { date: PropTypes.instanceOf(Date), onDateChanged: PropTypes.func, };
The default calendar export, as well as other helper functions and constants, have been imported from the calendar helpers
module. All of the exports from the calendar styles module have been imported with the Styled
namespace.
At the moment, we haven’t created any styles, but we will soon, using styled-components. The component state is partly resolved from props using the useState
Hook, which sets the state with an object containing the following:
current
: A Date
object for the currently selected date or nullmonth
: The month of the currently selected date if it is set. Otherwise, it defaults to the month of the current dateyear
: The year of the currently selected date if it is set. Otherwise, it defaults to the year of the current dateThe month
and year
state properties are required to properly render the calendar, as shown in the getCalendarDates()
method, which uses the calendar builder function to build the calendar for the month and year.
Finally, the state is augmented with the today
property, which is a Date
object for the current date.
calendar
componentIn the previous calendar
component code snippet, the renderer
part referenced methods for rendering the month and year, weekdays, and calendar dates. We’ll add these methods to the calendar
component below the getCalendarDates()
function as follows:
const renderMonthAndYear = () => { const { month, year } = dateState; const formatter = new Intl.DateTimeFormat('zh-CN', { day: 'numeric', month: 'short', year: 'numeric' }); const formattedDate = formatter.format(dateState.current); console.log('formatted date', formattedDate) // Resolve the month name from the CALENDAR_MONTHS object map const monthname = Object.keys(CALENDAR_MONTHS)[Math.max(0, Math.min(month - 1, 11))]; return ( <Styled.CalendarHeader> <Styled.ArrowLeft onClick={handlePrevious} title="Previous Month" /> <Styled.CalendarMonth> {monthname} {year} {/* {formattedDate} */} </Styled.CalendarMonth> <Styled.ArrowRight onClick={handleNext} title="Next Month" /> </Styled.CalendarHeader> ); }; // Render the label for day of the week // This method is used as a map callback as seen in render() const renderDayLabel = (day, index) => { // Resolve the day of the week label from the WEEK_DAYS object map const daylabel = WEEK_DAYS[day].toUpperCase(); return ( <Styled.CalendarDay key={daylabel} index={index}> {daylabel} </Styled.CalendarDay> ); }; // Render a calendar date as returned from the calendar builder function const renderCalendarDate = (date, index) => { const { current, month, year } = dateState; const _date = new Date(date.join("-")); // Check if calendar date is same day as today const isToday = isSameDay(_date, today); // Check if calendar date is same day as currently selected date const isCurrent = current && isSameDay(_date, current); // Check if calendar date is in the same month as the state month and year const inMonth = month && year && isSameMonth(_date, new Date([year, month, 1].join("-"))); // The click handler const onClick = gotoDate(_date); const props = { index, inMonth, onClick, title: _date.toDateString() }; // Conditionally render a styled date component const DateComponent = isCurrent ? Styled.HighlightedCalendarDate : isToday ? Styled.TodayCalendarDate : Styled.CalendarDate; return ( <DateComponent key={getDateISO(_date)} {...props}> {_date.getDate()} {/* {formattedDate} */} </DateComponent> ); };
In the renderMonthAndYear()
method, the name of the month is first resolved from the CALENDAR_MONTHS
object. Then, it is rendered alongside the year with two arrow controls on the left and right side for navigating.
Each arrow control has event handlers for the onClick
events, which we’ll define later as handlePrevious()
and handleNext()
. With some styling added, the rendered DOM from the renderMonthAndYear()
method looks like the following screenshot:
The renderDayLabel()
method renders a label for a day of the week, which it resolves from the WEEK_DAYS
object. It takes two arguments, day and index, because it is used as a callback function to .map()
, as seen in the render()
method.
After mapping, the rendered DOM for the days of the week looks like the image below:
The renderCalendarDate()
method is also used as a .map()
callback function and renders a calendar date. The date it receives as its first argument is in the format [YYYY, MM, DD]
.
The renderCalendarDate()
method checks if the date is the same as today’s date, including the current state’s month and year. With these checks, it conditionally renders one of the variants of the calendar date cell, HiglightedCalendarDate
, TodayCalendarDate
, or CalendarDate
.
For each rendered calendar date, an onClick
handler is set to jump to that particular date using the gotoDate()
method, which we’ll define in the next section.
We’ve made a couple of references to event handlers in the previous sections. Add the following code below the renderCalendarDate
function located inside the calendar
component:
const gotoDate = (date) => (evt) => { evt && evt.preventDefault(); const { current } = dateState; if (!(current && isSameDay(date, current))) { addDateToState(date); onDateChanged(date); } }; const gotoPreviousMonth = () => { const { month, year } = dateState; const previousMonth = getPreviousMonth(month, year); setDateState({ month: previousMonth.month, year: previousMonth.year, current: dateState.current, }); }; const gotoNextMonth = () => { const { month, year } = dateState; const nextMonth = getNextMonth(month, year); setDateState({ month: nextMonth.month, year: nextMonth.year, current: dateState.current, }); }; const handlePrevious = (evt) => { gotoPreviousMonth(); }; const handleNext = (evt) => { gotoNextMonth(); };
The gotoDate()
method is a higher-order function that takes a Date
object as its argument. It returns an event handler that can be triggered to update the currently selected date in the state. addStateToDate()
is used to resolve the month and year from the date and update the state.
If a callback function is passed to the onDateChanged
prop of the calendar
component, that function will be called with the updated date. This feature is especially helpful in cases where you want to propagate the date change to a parent component.
The handlePrevious()
and handleNext()
event handlers share a similar behavior. They allow the user to cycle through the months.
With our calendar
component complete, let’s add styled-components for styling. Add the following code snippet to the src/components/Calendar/styles.js
file:
import styled from "styled-components"; export const Arrow = styled.button` appearance: none; user-select: none; outline: none !important; display: inline-block; position: relative; cursor: pointer; padding: 0; border: none; border-top: 1.6em solid transparent; border-bottom: 1.6em solid transparent; transition: all 0.25s ease-out; `; export const ArrowLeft = styled(Arrow)` border-right: 2.4em solid #ccc; left: 1.5rem; :hover { border-right-color: #06c; } `; export const ArrowRight = styled(Arrow)` border-left: 2.4em solid #ccc; right: 1.5rem; :hover { border-left-color: #06c; } `; export const CalendarContainer = styled.div` font-size: 5px; border: 2px solid #06c; border-radius: 5px; overflow: hidden; `; export const CalendarHeader = styled.div` display: flex; align-items: center; justify-content: space-between; `; export const CalendarGrid = styled.div` display: grid; grid-template: repeat(7, auto) / repeat(7, auto); `; export const CalendarMonth = styled.div` font-weight: 500; font-size: 5em; color: #06c; text-align: center; padding: 0.5em 0.25em; word-spacing: 5px; user-select: none; `; export const CalendarCell = styled.div` text-align: center; align-self: center; letter-spacing: 0.1rem; padding: 0.6em 0.25em; user-select: none; grid-column: ${(props) => (props.index % 7) + 1} / span 1; `; export const CalendarDay = styled(CalendarCell)` font-weight: 600; font-size: 2.25em; color: #06c; border-top: 2px solid #06c; border-bottom: 2px solid #06c; border-right: ${(props) => (props.index % 7) + 1 === 7 ? `none` : `2px solid #06c`}; `; export const CalendarDate = styled(CalendarCell)` font-weight: ${(props) => (props.inMonth ? 500 : 300)}; font-size: 4em; cursor: pointer; border-bottom: ${(props) => (props.index + 1) / 7 <= 5 ? `1px solid #ddd` : `none`}; border-right: ${(props) => (props.index % 7) + 1 === 7 ? `none` : `1px solid #ddd`}; color: ${(props) => (props.inMonth ? `#333` : `#ddd`)}; grid-row: ${(props) => Math.floor(props.index / 7) + 2} / span 1; transition: all 0.4s ease-out; :hover { color: #06c; background: rgba(0, 102, 204, 0.075); } `; export const HighlightedCalendarDate = styled(CalendarDate)` color: #fff !important; background: #06c !important; position: relative; ::before { content: ""; position: absolute; top: -1px; left: -1px; width: calc(100% + 2px); height: calc(100% + 2px); border: 2px solid #06c; } `; export const TodayCalendarDate = styled(HighlightedCalendarDate)` color: #06c !important; background: transparent !important; ::after { content: ""; position: absolute; right: 0; bottom: 0; border-bottom: 0.75em solid #06c; border-left: 0.75em solid transparent; border-top: 0.75em solid transparent; } :hover { color: #06c !important; background: rgba(0, 102, 204, 0.075) !important; } `; export const BlockedCalendarDate = styled(CalendarDate)` color: black !important; background: gray !important; position: relative; :hover { color: black !important; background: gray !important; border-color:gray; cursor:default; } `;
Now, when you render the calendar
component in the app, it will look like the screenshot below:
datepicker
componentTo begin building our datepicker
component, add the following code snippet to the src/components/Datepicker/index.js
file:
import React, { useState, useEffect } from "react"; import PropTypes from "prop-types"; import Calendar from "../Calendar/index"; import * as Styled from "./styles"; import { isDate, getDateISO } from "../../helpers/calendar"; export default function Datepicker(props) { const [dateState, setDateState] = useState(); const [calendarOpen, setCalendarOpen] = useState(false); const { label } = props; const toggleCalendar = () => setCalendarOpen(!calendarOpen); const handleChange = (evt) => evt.preventDefault(); const handleDateChange = (date) => { const newDate = date ? getDateISO(date) : null; dateState !== newDate && setDateState(newDate) }; useEffect(() => { const newDate = new Date(); console.log('new date', newDate) setDateState(getDateISO(newDate)); }, []); const closeCalendar = () => { setCalendarOpen(false) } } Datepicker.propTypes = { label: PropTypes.string, value: PropTypes.string, onDateChanged: PropTypes.func, };
The component state is initialized with two properties:
date
: An ISO string representation for the current date, formatted as "YYYY-MM-DD"
calendarOpen
: A Boolean flag that indicates whether the datepicker calendar is visibleWhen the component mounts, the Date
object is resolved from the value prop passed to the component, then it is updated on the state, as seen in the useEffect
function.
The handleDateChange
method takes a Date
object as its argument and updates the date in the state. If a callback function is passed to the onDateChanged
prop of the datepicker
component, then that function will be called with the updated ISO date string.
It’s worth mentioning that the Bootstrap dropdown component will be used to simulate the dropdown effect for our custom datepicker, which is the primary purpose of adding the Reactstrap package as a dependency in our project. The styled components rendered in the datepicker are styled extensions of the dropdown components from Reactstrap.
Update the datepicker
component to include the render()
method, as shown in the following code snippet:
return ( <Styled.DatePickerContainer > <Styled.DatePickerFormGroup> <Styled.DatePickerLabel>{label || "Enter Date"}</Styled.DatePickerLabel> <Styled.DatePickerInput type="text" value={dateState ? dateState.split("-").join(" / ") : ""} onChange={handleChange} readOnly="readonly" placeholder="YYYY / MM / DD" /> </Styled.DatePickerFormGroup> <Styled.DatePickerDropdown isOpen={calendarOpen} toggle={toggleCalendar}> <Styled.DatePickerDropdownToggle color="transparent" /> <Styled.DatePickerDropdownMenu> {calendarOpen && ( <div> <Calendar date={dateState && new Date(dateState)} onDateChanged={handleDateChange} /> <Styled.DatePickerButton onClick={closeCalendar}>Save</Styled.DatePickerButton> </div> )} </Styled.DatePickerDropdownMenu> </Styled.DatePickerDropdown> </Styled.DatePickerContainer> );
The Styled.DatePickerFormGroup
component is a Bootstrap .form-group
that wraps the datepicker
label and input
field. It’s important to note that the input field is the "text"
type and marked as readonly
, so you can’t edit it directly. Also, notice that the default behavior for the change event on the input element has been prevented.
The Styled.DatePickerDropdown
component and its descendants are styled extensions of the dropdown component from the Reactstrap package.
Finally, the calendar
component is rendered in the dropdown menu, passing the date from the state and the handleDateChange()
method as a callback function for the onDateChanged
prop.
The final rendered DOM for the datepicker
component should look like the following screenshot, with some styling:
Add the following code snippet to the src/components/Datepicker/styles.js
file to create the styled components required for the datepicker:
import styled from "styled-components"; import { FormGroup, Label, Input, Button, Dropdown, DropdownToggle, DropdownMenu, } from "reactstrap"; export const DatePickerContainer = styled.div` position: relative; `; export const DatePickerFormGroup = styled(FormGroup)` display: flex; justify-content: space-between; position: relative; width: 100%; border: 2px solid #06c; border-radius: 5px; overflow: hidden; `; export const DatePickerLabel = styled(Label)` margin: 0; padding: 0 2rem; font-weight: 600; font-size: 0.7rem; letter-spacing: 2px; text-transform: uppercase; color: #06c; border-right: 2px solid #06c; display: flex; align-items: center; justify-content: center; background: rgba(0, 102, 204, 0.05); `; export const DatePickerInput = styled(Input)` font-weight: 500; font-size: 1rem; color: #333; box-shadow: none; border: none; text-align: center; letter-spacing: 1px; background: transparent !important; display: flex; align-items: center; ::placeholder { color: #999; font-size: 0.9rem; } width:100%; height:100%; `; export const DatePickerDropdown = styled(Dropdown)` position: absolute; width: 100%; height: 100%; top: 0; left: 0; `; export const DatePickerDropdownToggle = styled(DropdownToggle)` position: relative; width: 100%; height: 100%; background: transparent; opacity: 0; filter: alpha(opacity=0); `; export const DatePickerDropdownMenu = styled(DropdownMenu)` margin-top: 3rem; left: 0; width: 100%; height: 75vh !important; border: none; padding: 0; transform: none !important; `; export const DatePickerButton = styled(Button)` position: absolute; border: 2px solid #06c; margin-top:2%; right:50% !important; background: transparent; font-size: 1.2rem; color: #06c; :hover { border: white solid #06c; color: white !important; background: #06c; } `;
app
componentFinally, update the src/App.js
file to look like the following code snippet:
import "./App.css"; function App() { return ( <div className="App container"> <div className="mt-2 w-100 h-50 d-flex justify-content-center row"> <div className="col-6"> </div> </div> </div> ); } export default App;
If you have followed this tutorial, you should have a working, custom datepicker rendered in your React application. Now, render it inside the app
component:
import "./App.css"; import Datepicker from "./components/Datepicker"; function App() { return ( <div className="App container"> <div className="mt-2 w-100 h-50 d-flex justify-content-center row"> <div className="col-6"> <Datepicker label="From" /> </div> </div> </div> ); } export default App;
Here’s our final result:
In this section, we will add a few advanced features to our custom datepicker such as date range selection, a time picker, and the ability to block certain dates in the calendar.
To add a date range selection feature, we will use just two Datepicker
components. Go back to your App.js
file and replace the App
component code with the following:
function App() { return ( <div className="App container"> <div className="mt-2 w-100 h-50 d-flex justify-content-center row"> <div className="col"> <Datepicker label="From" /> </div> <div className="col"> <Datepicker label="To" /> </div> </div> </div> ); }
Here, you added two Datepicker
components instead of one to the App
component with the labels From
and To
:
To add the time picker feature, you will first need to create a directory named TimePicker
inside the Components
directory:
mkdir -p src/components/Timepicker
Next, inside this directory, create two files named index.js
and styles.js
:
touch src/components/Timepicker/index.js src/components/Timepicker/styles.js
Add the following code to the src/components/Timepicker/index.js
file:
import React, { useState } from "react"; import PropTypes from "prop-types"; import { ListGroup, ListGroupItem, } from "reactstrap"; import * as Styled from "./styles"; const hours = Array.from({ length: 12 }, (_, i) => i + 1); const minutes = Array.from({ length: 60 }, (_, i) => i); const dayPeriods = ["AM", "PM"]; export default function TimePicker({ handleTimeChange }) { const [timeState, setTimeState] = useState({ hour: 12, minutes: 0, period: 'AM', }); const handleHourChange = (event, hour) => { console.log('hour change', hour) const newState = { hour: hour, minutes: timeState.minutes, period: timeState.period, } setTimeState(newState) setTimeout(() => { handleTimeChange(newState) }, 100); } const handleMinutesChange = (event, minutes) => { console.log('minutes change', minutes) const newState = { hour: timeState.hour, minutes: minutes, period: timeState.period, } setTimeState(newState) handleTimeChange(newState) } const handlePeriodChange = (event, period) => { console.log('minutes change', period) const newState = { hour: timeState.hour, minutes: timeState.minutes, period: period, } setTimeState(newState) handleTimeChange(newState) } return ( <div className="d-flex flex-row"> <Styled.TimePickerListContainer className="border-end border-primary "> <ListGroup className="" > {hours.map((hour, index) => ( <ListGroupItem key={index} tag="button" action className="border-0" onClick={(event) => handleHourChange(event, hour)}>{hour}</ListGroupItem> ))} </ListGroup> </Styled.TimePickerListContainer> <Styled.TimePickerListContainer className="border-end border-primary"> <ListGroup className="" > {minutes.map((minute, index) => ( <ListGroupItem key={index} tag="button" action className="border-0" onClick={(event) => handleMinutesChange(event, minute)}>{minute}</ListGroupItem> ))} </ListGroup> </Styled.TimePickerListContainer> <Styled.TimePickerListContainer className=""> <ListGroup className="" > {dayPeriods.map((period, index) => ( <ListGroupItem key={index} tag="button" action className="border-0 pe-4" onClick={(event) => handlePeriodChange(event, period)}>{period}</ListGroupItem> ))} </ListGroup> </Styled.TimePickerListContainer> </div > ) } TimePicker.propTypes = { handleTimeChange: PropTypes.func, };
This TimePicker
component above is designed to allow users to select a time, providing options for hours, minutes, and the period (AM or PM). It uses React’s useState
Hook to manage the selected time state and PropTypes for ensuring proper type checking of props.
The component expects a handleTimeChange
prop, which is a function responsible for updating the selected time state in the parent component whenever a change occurs. This function is crucial for keeping the parent component in sync with the TimePicker
‘s state.
To populate the selectable options, the component generates arrays for hours (1-12), minutes (0-59), and periods (AM and PM) using Array.from
. These arrays are then mapped to create ListGroupItems
, which are styled to resemble buttons. Each button is clickable and triggers the respective event handler (handleHourChange
, handleMinutesChange
, handlePeriodChange
) when clicked.
The event handlers update the selected time state based on the user’s selection, ensuring that the displayed time accurately reflects the user’s choice. Additionally, a timeout is used to delay the handleTimeChange
function call to prevent the rapid firing of events while the user is selecting a time.
Let’s add a single styled component for styling. Add the following code snippet to the src/components/Timepicker/styles.js
file:
import styled from "styled-components"; export const TimePickerListContainer = styled.div` height: 75vh !important; overflow-y: auto; `;
Now it’s time to add the Timepicker
component inside the Datepicker
component. Open the src/components/Datepicker/index.js
file and import Timepicker
:
import TimePicker from "../Timepicker";
Next, add the following code below the label
constant located inside Datepicker
:
const [timeState, setTimeState] = useState({ hour: 12, minutes: 0, period: 'AM', }); const handleTimeChange = (time) => { const newTime = time ? time : null; if (newTime !== null && JSON.stringify(timeState) !== JSON.stringify(newTime)) { setTimeState(newTime) } }; const formatTime = (time) => { const hour = time.hour < 10 ? '0' + time.hour : time.hour const minutes = time.minutes < 10 ? '0' + time.minutes : time.minutes const formattedTime = hour + ' : ' + minutes + ' ' + time.period return formattedTime }
This code snippet sets up a timeState
state variable using React’s useState
Hook. The timeState
object contains three properties: hour
, minutes
, and period
, initialized to 12
, 0
, and AM
respectively. This state variable will hold the currently selected time.
Additionally, two functions named handleTimeChange
and formatTime
were defined.
The handleTimeChange
function takes a time
parameter and is called inside the Timepicker
component. This function is responsible for updating the timeState
based on the new time
value. It first checks if the time
parameter is not null
and if the new time
is different from the current timeState
. If these conditions are met, it updates timeState
using setTimeState
.
The formatTime
function also takes a time
parameter, and it will be called inside the render
function to format the time.
Now, go to the render
function and update it to include the Timepicker
:
return ( <Styled.DatePickerContainer > <Styled.DatePickerFormGroup> <Styled.DatePickerLabel>{label || "Enter Date"}</Styled.DatePickerLabel> <Styled.DatePickerInput type="text" value={dateState ? dateState.split("-").join(" / ") : ""} onChange={handleChange} readOnly="readonly" placeholder="YYYY / MM / DD" /> <Styled.DatePickerInput type="text" value={timeState ? formatTime(timeState) : ""} onChange={handleChange} readOnly="readonly" placeholder="12 : 00 AM" /> </Styled.DatePickerFormGroup> <Styled.DatePickerDropdown isOpen={calendarOpen} toggle={toggleCalendar}> <Styled.DatePickerDropdownToggle color="transparent" /> <Styled.DatePickerDropdownMenu> {calendarOpen && ( <div > <div className="row"> <div className="col-9"> <Calendar date={dateState && new Date(dateState)} onDateChanged={handleDateChange} /> </div> <div className="col-3 pe-4"> <TimePicker handleTimeChange={handleTimeChange} className="border border-primary"/> </div> </div> <Styled.DatePickerButton onClick={closeCalendar}>Save</Styled.DatePickerButton> </div> )} </Styled.DatePickerDropdownMenu> </Styled.DatePickerDropdown> </Styled.DatePickerContainer> );
After the update, the component now renders two input fields, one input field for the date and another for the time. The selected date is displayed in the date input field. The time input field displays the selected time, which can be changed using the TimePicker
component.
The TimePicker
component was added below the Calendar
component and it receives the handleTimeChange
function as a prop. Here is the adjusted datepicker with the time picker option:
To block certain dates from being selected in the calendar, you will need to make a couple of changes to the Datepicker
and Calendar
components.
Open the src/components/Datepicker/index.js
file and add the following code below the closeCalendar
function:
const blockedDates = [ new Date('2024-04-7'), new Date('2024-04-13'), new Date('2024-04-15'), new Date('2024-04-20') ];
Above, you added an array containing the dates you’d like to block in the calendar.
Next, go to the render function and pass the blockedDates
to the Calendar
component as a prop:
<Calendar date={dateState && new Date(dateState)} onDateChanged={handleDateChange} blockedDates={blockedDates} />
Now, open the src/components/Calendar/index.js
file and update it with the following code:
export default function Calendar({ date, onDateChanged, blockedDates }) { ... const renderCalendarDate = (date, index) => { const { current, month, year } = dateState; const _date = new Date(date.join("-")); // Check if calendar date is same day as today const isToday = isSameDay(_date, today); // block date const isBlocked = blockedDates.some(blockedDate => isSameDay(_date, blockedDate)); // Check if calendar date is same day as currently selected date const isCurrent = current && isSameDay(_date, current); // Check if calendar date is in the same month as the state month and year const inMonth = month && year && isSameMonth(_date, new Date([year, month, 1].join("-"))); // The click handler const onClick = isBlocked ? null : gotoDate(_date); const props = { index, inMonth, onClick, title: _date.toDateString() }; // Conditionally render a styled date component const DateComponent = isCurrent ? Styled.HighlightedCalendarDate : isToday ? Styled.TodayCalendarDate : !isBlocked ? Styled.CalendarDate : Styled.BlockedCalendarDate; return ( <DateComponent key={getDateISO(_date)} {...props}> {_date.getDate()} {/* {formattedDate} */} </DateComponent> ); }; Calendar.propTypes = { date: PropTypes.instanceOf(Date), onDateChanged: PropTypes.func, blockedDates: PropTypes.arrayOf(Date) };
In the updated code, first, blockedDates
was added as a prop to the Calendar
component.
Next, the code modifies the renderCalendarDate
function to also check if the date that has to be rendered is included in blockedDates
. If that is the case, the BlockedCalendarDate
styled component will be rendered to indicate that the date is blocked and a null onClick
event listener will be assigned to it, preventing the user from selecting the date.
Lastly, blockedDates
was added to the Calendar PropTypes
:
You saw firsthand that implementing a custom datepicker from scratch can be a complex task, but luckily that isn’t the only option. In this section, we will try some pre-made datepicker components.
React Date Picker is a popular component that allows users to select dates easily in a web application. It provides a visual calendar interface for selecting dates and can be customized to fit the look and feel of the application.
Run one of the following commands to install the component if you are using npm or Yarn:
npm install react-datepicker
yarn add react-datepicker
To use this component in your application, you will need to import the component and the CSS associated with this package.
Here is what your App.js
file should look like:
import "./App.css"; import React, { useState } from "react"; import DatePicker from "react-datepicker"; import "react-datepicker/dist/react-datepicker.css"; function App() { const [startDate, setStartDate] = useState(new Date()); return ( <div className="mt-2 w-100 h-50 d-flex justify-content-center"> <DatePicker selected={startDate} onChange={(date) => setStartDate(date)} /> </div> ); } export default App;
Let’s add the ability for a user to select dates within a specific range:
function App() { const [startDate, setStartDate] = useState(new Date()); const [endDate, setEndDate] = useState(null); const onChange = (dates) => { const [start, end] = dates; setStartDate(start); setEndDate(end); }; return ( <div className="mt-2 w-100 h-50 d-flex justify-content-center"> <DatePicker selected={startDate} onChange={onChange} startDate={startDate} endDate={endDate} selectsRange inline /> </div> ); }
MUI is a freemium React component library that provides ready-to-use components following Google’s Material Design guidelines.
Run the following commands to install the MUI Date and Time Pickers library if you are using npm or Yarn:
npm install @mui/x-date-pickers @mui/material @emotion/react @emotion/styled // Install date library (if not already installed) npm install dayjs yarn add @mui/x-date-pickers @mui/material @emotion/react @emotion/styled // Install date library (if not already installed) yarn add dayjs
Before rendering any components that require date or time selection, the MUI documentation recommends placing a LocalizationProvider
higher in the React component tree. This component is responsible for integrating your selected date library adapter — AdapterDayjs
, which leverages Day.js — with the Date and Time Picker components throughout your application by using the React Context system. This setup ensures that date and time functionality is uniformly available across your UI components.
Here is what your App.js
file should look like:
import "./App.css"; import { LocalizationProvider } from '@mui/x-date-pickers'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs' import { DatePicker } from '@mui/x-date-pickers/DatePicker'; function App() { return ( <LocalizationProvider dateAdapter={AdapterDayjs}> <div className="mt-2 w-100 h-50 d-flex justify-content-center"> <DatePicker /> </div> </LocalizationProvider> ); } export default App;
This library’s date and time picker components are available for free but the date range components are only available on the paid Pro plan.
In this tutorial, we covered how to set up a datepicker in React that acts as an alternative to the native HTML5 datepicker
input element. With a straightforward datepicker, your users are less likely to format date inputs incorrectly, improving the quality of the information provided to your application.
You can customize and upgrade the datepicker from this tutorial to best suit your application. I hope you enjoyed this article. Happy coding!
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
2 Replies to "Creating a custom React datepicker"
Unfortunately this datepicker does not work so well on an iPhone in Safari, the “isSameMonth” function breaks, for example.. I tried the Codesandbox demo on my phone.
I think there’s a bug. Just loaded up the git code and clicking the arrows to change the month causes an infinite cycle.