Create a custom React date picker in 10 minutes
In this article, I'll show you the easiest way possible to create a date picker using React. We'll be using:
This will be the end result.
I'm assuming that you know basic JavaScript and that you've encountered a little bit of React and hooks before. Other than that, there are no prerequisites.
Let's get started!
Step 1: Install dependencies
First, we need to install our dependencies. Like we mentioned before, we'll use emotion and @datepicker-react/hooks.
yarn add @emotion/core @datepicker-react/hooks
Step 2: Datepicker context
We'll create a Context
to share the date picker state and callbacks through the date picker component tree.
Context provides a way to pass data through the component tree without having to pass props down manually at every level.
// datepickerContext.jsimport React from "react";export const datepickerContextDefaultValue = {focusedDate: null,isDateFocused: () => false,isDateSelected: () => false,isDateHovered: () => false,isDateBlocked: () => false,isFirstOrLastSelectedDate: () => false,onDateFocus: () => {},onDateHover: () => {},onDateSelect: () => {}};export default React.createContext(datepickerContextDefaultValue);
The upper code creates a Context object, which contains default state and callbacks. When React renders a component that subscribes to this Context
object it will read the current context value from the closest matching Provider above it in the tree. If there isn't a matching Provider
, then the current Context will read default values.
Step 3: NavButton component
In this step, we'll create a button, which will allow us to navigate through months. A Button
component will receive two props, children
and onClick
callback.
// NavButton.js/** @jsx jsx */import { jsx } from "@emotion/core";export default function NavButton({ children, onClick }) {return (<buttontype="button"onClick={onClick}css={{border: "1px solid #929598",background: "transparent",padding: "8px",fontSize: "12px"}}>{children}</button>);}
Step 4: Datepicker component
This will be our core component, which will contain our date picker logic. Date manipulation isn't the easiest job to do in a JavaScript. That's why people use libraries like moment, date-fns and luxon.
Luckily, we don't have to use any of them, because we use @dapicker-react/hooks, which does all the dirty job for us. We need just to use the right hook and pass the proper arguments.
@dapicker-react/hooks has three exported hooks:
- useDatepicker
- useMonth
- useDay
We can understand from the name, that we will use useDatepicker
hook in Datepicker
component.
So let's program the Datepicker
component.
1. Define state
We need to manage the start and end date, which we can do with the useState
hook.
// Datepicker.jsimport React, { useState } from "react";import { START_DATE } from "@datepicker-react/hooks";function Datepicker() {const [state, setState] = useState({startDate: null,endDate: null,focusedInput: START_DATE});return null}export default Datepicker;
We set the initial state value. We have an empty start and end date. We see that we also have focusedInput
in the state object,
with which we determine or choose the start or end date.
2. useDatepicker hook
We can pass various arguments (see all)
to the useDatepicker
hook, but we will pass just the required ones. useDatepicker
return an object, which includes callbacks and
variables, that we need to manage the date picker. Some of them we'll pass to the Context
, which we did in step 2.
import React, { useState } from "react";import { useDatepicker, START_DATE } from "@datepicker-react/hooks";function Datepicker() {const [state, setState] = useState({startDate: null,endDate: null,focusedInput: START_DATE});const {firstDayOfWeek,activeMonths,isDateSelected,isDateHovered,isFirstOrLastSelectedDate,isDateBlocked,isDateFocused,focusedDate,onDateHover,onDateSelect,onDateFocus,goToPreviousMonths,goToNextMonths} = useDatepicker({startDate: state.startDate,endDate: state.endDate,focusedInput: state.focusedInput,onDatesChange: handleDateChange});function handleDateChange(data) {if (!data.focusedInput) {setState({ ...data, focusedInput: START_DATE });} else {setState(data);}}return null}export default Datepicker;
3. Final Datepicker component
/** @jsx jsx */import { useState } from "react";import { useDatepicker, START_DATE } from "@datepicker-react/hooks";import { jsx } from "@emotion/core";import Month from "./Month";import NavButton from "./NavButton";import DatepickerContext from "./datepickerContext";function Datepicker() {const [state, setState] = useState({startDate: null,endDate: null,focusedInput: START_DATE});const {firstDayOfWeek,activeMonths,isDateSelected,isDateHovered,isFirstOrLastSelectedDate,isDateBlocked,isDateFocused,focusedDate,onDateHover,onDateSelect,onDateFocus,goToPreviousMonths,goToNextMonths} = useDatepicker({startDate: state.startDate,endDate: state.endDate,focusedInput: state.focusedInput,onDatesChange: handleDateChange});function handleDateChange(data) {if (!data.focusedInput) {setState({ ...data, focusedInput: START_DATE });} else {setState(data);}}return (<DatepickerContext.Providervalue={{focusedDate,isDateFocused,isDateSelected,isDateHovered,isDateBlocked,isFirstOrLastSelectedDate,onDateSelect,onDateFocus,onDateHover}}><div><strong>Focused input: </strong>{state.focusedInput}</div><div><strong>Start date: </strong>{state.startDate && state.startDate.toLocaleString()}</div><div><strong>End date: </strong>{state.endDate && state.endDate.toLocaleString()}</div><NavButton onClick={goToPreviousMonths}>Previous</NavButton><NavButton onClick={goToNextMonths}>Next</NavButton><divcss={{display: "grid",margin: "32px 0 0",gridTemplateColumns: `repeat(${activeMonths.length}, 300px)`,gridGap: "0 64px"}}>{activeMonths.map(month => (<Monthkey={`${month.year}-${month.month}`}year={month.year}month={month.month}firstDayOfWeek={firstDayOfWeek}/>))}</div></DatepickerContext.Provider>);}export default Datepicker;
Step 5: Month component
Probably you see that Month
component is included into Datepicker
component and it gets three props year
, month
and firstDayOfWeek
.
All of these we will pass to the useMonth
hook, which will return days of the month, week labels and month label.
/** @jsx jsx */import { useMonth } from "@datepicker-react/hooks";import { jsx } from "@emotion/core";import Day from "./Day";function Month({ year, month, firstDayOfWeek }) {const { days, weekdayLabels, monthLabel } = useMonth({year,month,firstDayOfWeek});return (<div><div css={{ textAlign: "center", margin: "0 0 16px" }}><strong>{monthLabel}</strong></div><divcss={{display: "grid",gridTemplateColumns: "repeat(7, 1fr)",justifyContent: "center",marginBottom: "10px",fontSize: "10px"}}>{weekdayLabels.map(dayLabel => (<div css={{ textAlign: "center" }} key={dayLabel}>{dayLabel}</div>))}</div><divcss={{display: "grid",gridTemplateColumns: "repeat(7, 1fr)",justifyContent: "center"}}>{days.map((day, index) => {if (typeof day === "object") {return (<Daydate={day.date}key={day.date.toString()}dayLabel={day.dayLabel}/>);}return <div key={index} />;})}</div></div>);}export default Month;
Step 6: Day component
We are close to the finish line, we need just to do the Day
component.
It will receive date
and dayLabel
props. We will pass date prop along with callbacks from date picker context (step 2) to the useDay
hook.
/** @jsx jsx */import { useRef, useContext } from "react";import { useDay } from "@datepicker-react/hooks";import { jsx } from "@emotion/core";import DatepickerContext from "./datepickerContext";function getColor(isSelected,isSelectedStartOrEnd,isWithinHoverRange,isDisabled) {return ({selectedFirstOrLastColor,normalColor,selectedColor,rangeHoverColor,disabledColor}) => {if (isSelectedStartOrEnd) {return selectedFirstOrLastColor;} else if (isSelected) {return selectedColor;} else if (isWithinHoverRange) {return rangeHoverColor;} else if (isDisabled) {return disabledColor;} else {return normalColor;}};}function Day({ dayLabel, date }) {const dayRef = useRef(null);const {focusedDate,isDateFocused,isDateSelected,isDateHovered,isDateBlocked,isFirstOrLastSelectedDate,onDateSelect,onDateFocus,onDateHover} = useContext(DatepickerContext);const {isSelected,isSelectedStartOrEnd,isWithinHoverRange,disabledDate,onClick,onKeyDown,onMouseEnter,tabIndex} = useDay({date,focusedDate,isDateFocused,isDateSelected,isDateHovered,isDateBlocked,isFirstOrLastSelectedDate,onDateFocus,onDateSelect,onDateHover,dayRef});if (!dayLabel) {return <div />;}const getColorFn = getColor(isSelected,isSelectedStartOrEnd,isWithinHoverRange,disabledDate);return (<buttononClick={onClick}onKeyDown={onKeyDown}onMouseEnter={onMouseEnter}tabIndex={tabIndex}type="button"ref={dayRef}css={{padding: "8px",border: 0,color: getColorFn({selectedFirstOrLastColor: "#FFFFFF",normalColor: "#001217",selectedColor: "#FFFFFF",rangeHoverColor: "#FFFFFF",disabledColor: "#808285"}),background: getColorFn({selectedFirstOrLastColor: "#00aeef",normalColor: "#FFFFFF",selectedColor: "#71c9ed",rangeHoverColor: "#71c9ed",disabledColor: "#FFFFFF"})}}>{dayLabel}</button>);}export default Day;
Congrats! You created a basic date picker component and get a basic understanding of the React context and hooks.
I demonstrate how easy is to create a date picker with @datepicker-react/hooks library. If you want to use internationalizable, accessible, mobile-friendly date picker, you can use a @datepicker-react/styled library.