DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Brian-1150
Brian-1150

Posted on • Updated on

How to Create Custom React Calendar Date Range Picker

A few weeks into my first ever programming job, I was assigned the task of creating a form. The purpose of the form was to gather a few pieces of information from the user so they can perform a search. The user needs to choose a location from a dropdown and pick the start and end date along with times.

Being new to React and javascript in general, I just started out with a very basic form.

<form>
  <input type="text" 
   name="location"
   />
  <input type="date" 
   name="start"
   />
<input type="date" 
   name="end"
   />
<button type="submit">Search</button>
</form> 
Enter fullscreen mode Exit fullscreen mode

I began hitting hurdles which I believe are common to many React developers. How do I customize and style the form? I have a design mocked up that I have to follow and that requires specific colors, shapes, fonts, etc. and native html forms just don't give the developer much control.

I spent time researching and found all kinds of tutorials and third party libraries to try. I tried free ones, free trials of paid ones, open source ones. They each have their own learning curve just to be able to use them. It was time-consuming researching and experimenting, but it was a valuable experience just learning how to work with and implement third-party components. At the end of the day, however, I was just not able to overcome the barriers to customize the form and inputs to exactly how I needed them, specifically the date pickers.

I showed my team a "pretty good" version using react-hook-forms and react-datepicker, but when they asked me to move and resize and reshape and change colors, it was like I had to hack and custom build and add !important everywhere to override built in CSS, so much that is was decided it would be more efficient to build it from scratch.

Although the finished form had some really cool time pickers and sliders and custom drop downs with autofill, the focus of this tutorial will be on the calendar/date-range-picker portion.

To extract just the most important parts, and to keep it simple, I am starting out with npx create-react-app my-app --template typescript and removing some unnecessary files and logos.

If you want to TLDR and skip straight to the finished code, feel free to do so here. Or if you want to implement my published version, which has more features than this demo, it can be found on npm or simply npm i date-range-calendar.

I am starting out with a similar bare bones form to the one above, just to lay a foundation and work up from there in small incremental steps.
Scaffold out a form with a few inputs and a submit button in App.tsx. Make a few divs so we can separate the form from the rest of the page. Something like this should suffice

 import React from 'react';
import './App.css';

const [form, formSet] = useState({
    destination: '',
    start: '',
    end: ''
  });

  function handleSubmit() {
    console.log(form);
  }

 function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
    formSet({ ...form, [e.target.name]: e.target.value });
  }

function App() {
  return (
    <div className='container'>
      <form className='form' >
        <div className='input'>
          <input type="text"
            name="location"
          />
        </div>
        <div className='input'>
          <input type="date"
            name="start"
          />
        </div>
        <div className='input'>
          <input type="date"
            name="end"
          />
        </div>
        <div className='input'>
          <button onClick={handleSubmit} type='button'>Search</button>
        </div>
      </form>

    </div >
  )
}

export default App;

Enter fullscreen mode Exit fullscreen mode

Add some style to App.css and put some borders around our divs to help visualize the placement of the form on our page.

.App {
  text-align: center;
}

.container {
  border: 5px solid rgb(46, 57, 110);
  margin: 25px auto;
  height: 600px;
  width: 500px;
}

.form {
  border: 2px solid blue;
  height: 300px;
  width: 300px;
  margin: 25px auto;
  padding: 5px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
Enter fullscreen mode Exit fullscreen mode

We now have a basic form with three inputs, some state values, an onChange handler, and a submit handler. So the native input for type date is your basic default datePicker. It is perfectly functional, but we need something more modern and with more style.

Let's remove the two date inputs and replace them with a component. Create a new file called DatePicker.tsx.

And build a basic react component. This component will take some props so it can set the values in the parent. Let's start out with some hard coded numbers so we can get an idea of what this might look like:

import React from "react"
import './DatePicker.css';

type DatePickerProps = {

}

const DatePicker = (props: DatePickerProps) => {

    return (
        <>
            <div className='content'>
                <div className="calendar">
                    <h4>June 2022</h4>
                    <div className="row"> 1  2  3  4  5  6  7</div>
                    <div className="row"> 8  9 10 11 12 13 14</div>
                    <div className="row">15 16 17 18 19 20 21</div>
                    <div className="row">22 23 24 25 26 27 28</div>
                    <div className="row">29 30 31</div>
                </div>
            </div>
        </>
    )
};

export default DatePicker;

Enter fullscreen mode Exit fullscreen mode

And here is some style for DatePicker.css

.content {
    position: absolute;
    top: 65%;
    bottom: 10%;
    left: 50%;
    transform: translate(-50%, 0);
    background: #fff;
    overflow: auto;
    border: 1px solid #ccc;
    border-radius: 11px;
}

.calendar{
    width: 90%;
    display: flex;
    flex-flow: column nowrap;
    align-items: center;
}
.row{
    display: flex;
    flex-flow: row nowrap;
    justify-content: space-between;
    align-items: center;
}
Enter fullscreen mode Exit fullscreen mode

Now back in App.tsx, we want our calendar to pop up whenever the user clicks 'start' or 'end', so add another state value
const [open, openSet] = useState(false);
And conditionally render that child component dependent on the open value.

 <div onClick={() => openSet(!open)}>{form.start}</div>
 <div onClick={() => openSet(!open)}>{form.end}</div>
  {open && <DatePicker />}
Enter fullscreen mode Exit fullscreen mode

Now the calendar opens and closes by clicking either start or end. We are building a dateRangePicker so we want the calendar to open and let the user choose both dates.

Next step is to build a real calendar and populate it with real values accounting for leap year etc. Make a new file called CalendarUtils.ts and we will keep all our helper methods here. We will have an array of the month names:
const months = [" ", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
The calendar will have months. Each month will have either 4, 5, or 6 rows. And each row will have 7 individual blocks or boxes. Most of them will have days, but a few at the beginning and end will be blank. Let's start at the box level and work our way up. A box will simply be a styled

element with a value.

type BoxProps = {
    value: number | string
   }

 function Box(props: BoxProps) {

    return (
        <p>
            {props.value}
        </p>
    );
}
Enter fullscreen mode Exit fullscreen mode

Next, let's render some Boxes in another component call Row. Each row will have 7 boxes. So let's create a number[] with a for loop and slice it and map over it to create a row of 7 boxes. We need to specify what number each row will start and end with so we'll pass those values as props.

type IRowProps = {
startIndex: number
endIndex: number
}

function CalendarRow(props: IRowProps) {
    const dates: number[] = [];
    for (let i = 1; i < 32; i++) {
        dates.push(i);
    }
    return (
        <>
            <div >
                {
                  dates.slice(props.startIndex, props.endIndex).map((d, index) =>
                        <Box value={d} />
                    )
                }
            </div>
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

As you can see, we create an interface or prop type for every component by default so we can easily pass props as we discover what needs to be passed.

Now we need a Month component to render these rows. Each row needs the indexes, which we will hard code for now, as well as the month and year.

type IMonthProps = {

}

function Month(props: IMonthProps) {

    return (
        <>
            <h4 className='month'>February 2026</h4>
            <div className='days'>Sun Mon Tue Wed Thu Fri Sat</div>
            <div className='calendar'>
                <Row startIndex={0} endIndex={6}/>
                <Row startIndex={7} endIndex={13}/>
                <Row startIndex={14} endIndex={19}/>
                <Row startIndex={21} endIndex={25}/>
            </div>
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

And update some style in DatePicker.css

.calendar {
    display: flex;
    flex-flow: column nowrap;
    align-items: center;
}
.month{
    margin:6px;
}
.row {
    display: flex;
    flex-flow: row nowrap;
    justify-content: space-between;
    align-items: center;
}
Enter fullscreen mode Exit fullscreen mode

Now we can go back to the main export function for the DatePicker and render this component. But then let's go ahead and
use a for loop to get 2 years of months to render.

const DatePicker = (props: DatePickerProps) => {
    let monthsArray = new Array();
       for (let i = 0; i < 24; i++) {
       monthsArray.push(<Month/>)
       }

    return (
        <>
            <div className='content'>
                <div className="calendar">
                   {monthsArray}
                </div>
            </div>
        </>
    )
};
Enter fullscreen mode Exit fullscreen mode

Next step is to start replacing some of our hard coded days and months with actual date values by using Javascript Date() object. We could use a library, Day.js would be my choice, but we can keep our dependencies to a minimum for now.

This is where the real fun begins. Let's update the Month component to take in a couple props so we can determine the actual start and end indexes for each row. Instead of passing the start and end index to Row.tsx, I want to handle that logic in the component itself, but I do want to specify which row number. Remember, we always need at least 4 rows, usually 5, and occasionally 6. Also we will need to write some helper methods as we go. We need a method that gives us an array the exact values of the given month and empty strings for the few days before an after. So we will need to pass to that method the number of days in the month and numerical day of the week that month starts on. We have to get creative with how to make these methods give us the right days and the accurate number of days in each month. Here is what I came up with. There are certainly other ways of accomplishing this, but this works and it is efficient.

type IMonthProps = {
month: number
year: number
}
Enter fullscreen mode Exit fullscreen mode

Then in our DatePicker, we will add some logic to determine the dates and pass those down to the Month component.

    const today = new Date()
    let month = today.getMonth() + 1
    let year = today.getFullYear()
    const numOfDays = new Date(today.getFullYear(), today.getMonth() + 1, 0).getDate();
    const monthName = firstDateOfMonth.toLocaleString('default', { month: 'long' })

    for (let i = 0; i < 24; i++) {
        monthsArray.push(<Month month={month} year={year} />)
        year = month == 12 ? year + 1 : year
        month++
    }
Enter fullscreen mode Exit fullscreen mode

Helpers

export const getNumberOfRows = (numberOfDaysInMonth: number, dayOfTheWeek: number) => {
    switch (numberOfDaysInMonth - (21 + (7 - dayOfTheWeek))) {
        case 0: return 4;
        case 8: case 9: return 6;
        default: return 5;
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we should refactor our Row.tsx to build a real dates[] with the right values.

tye IRowProps = {
    dayOfWeek: number
    numberOfDaysInMonth: number
    rowNumber: number
}
function Row(props: IRowProps) {
    const dates = getOneMonthArray(props.dayOfWeek, props.numberOfDaysInMonth)
    let rowNumber = 0;
    rowNumber += props.rowNumber;
    let startIndex: number = (rowNumber * 7);
    let endIndex = startIndex + 7;

    return (...)
}
Enter fullscreen mode Exit fullscreen mode

We now have a perfectly functioning calendar that defaults to the current month. You can easily customize the code to suit your use case. For example, the for loop can be updated to only loop once and therefore, only show the current month. Or instead of starting with today, you can start with a month from the past or future.

Ok. Great! We have a foundation to build on. The next phase will be making this calendar into an interactive datePicker or dateRangePicker. We will add onClick handlers, manage the state, highlight the chosen dates, etc. Remember, in our main App.tsx component is where we our keeping our state, with the main form. So we'll need to pass down setter functions to our DatePicker.tsx component. Since we broke this into so many components we either need to prop drill our setters and a couple values all the way down, or use context or a store like redux. For this demo, we will just prop drill it down. In App.tsx, write two more functions. One we will call handleClick() and this we will pass all the way down to our Box.tsx component and assign it to the

in the return.

What this function needs to do is, capture and handle the date that was clicked, including month, day, and year. Since we are making a DateRangePicker, not just a DatePicker, we need to know if a date was already chosen. Furthermore, we need to determine, if one or both dates have already been chosen and if the date clicked is less than or greater than. I like to start with the onClick in the

tag of Box.tsx to see what kind of info I have access to and what the type is. If you add onClick={(e)=> console.log(e)} you will see everything you have access to everything you need. Add the following functions to App.tsx

const [startDate, startDateSet] = useState<Date | undefined>(undefined);
  const [endDate, endDateSet] = useState<Date | undefined>(undefined);


  function handleCalenderClicks(e: React.MouseEvent<HTMLDivElement>, value: string) {
    let p = e.target as HTMLDivElement
    if (!(startDate && !endDate)) {
      startDateSet(new Date(value))
      formSet({ ...form, start: value as string, end: 'end' })
      endDateSet(undefined)
      resetDivs()
      p.style.color = 'green'
      p.style.backgroundColor = 'lightblue'
    }
    else if (new Date(value) >= startDate) {
      endDateSet(new Date(value))
      formSet({ ...form, end: value as string })
      p.style.color = 'red'
      p.style.backgroundColor = 'lightblue'
    }
    else {
      startDateSet(new Date(value))
      formSet({ ...form, start: value as string })
      resetDivs()
      p.style.color = 'green'
      p.style.backgroundColor = 'lightblue'
    }
  }

  function resetDivs() {
    let container = document.querySelectorAll('p')
    container.forEach((div) => {
      let box = div as HTMLParagraphElement;
      if ((box.style.color == 'red' || 'green')) {
        box.style.color = 'inherit';
        box.style.fontWeight = 'inherit';
        box.style.backgroundColor = 'inherit';
      }
    })
  }

Enter fullscreen mode Exit fullscreen mode

As you can see here, we are accounting for all possible states our choices can be in and highlighting them and adding green color to our start and red to our end. The most common condition is to assign to the begin the date that was just clicked and reset everything else. I have chosen to use the document.querySelectorAll('p') in order to unhighlight previous choices, but be careful if you have other interactive

tags on the same page. If you have other

tags, but you don't manipulate their styles at all, then the resetDivs() function will not hurt them.

Be sure and add the function and month and year values to the prop types for each component as needed like:

    handleClick: (e: React.MouseEvent<HTMLDivElement>, value: string) => void
month: number
year: number
Enter fullscreen mode Exit fullscreen mode

and add them to the components as needed, for example:

                <Row month={props.month} year={props.year} handleClick={props.handleClick} dayOfWeek={dayOfWeek} numberOfDaysInMonth={numOfDays} rowNumber={0} />

Enter fullscreen mode Exit fullscreen mode

Thanks for reading and please let me know if you have any questions or comments.
Again, the links to the completed code can be found here. Or if you want to implement my published version, which has more features than this demo, it can be found on npm or simply 'npm i date-range-calendar'.

Top comments (2)

Collapse
 
kenramiscal1106 profile image
Ken Daniele Ramiscal

why use create-react-app?

Collapse
 
brian1150 profile image
Brian-1150 Author • Edited on

Thanks for reading @kenramiscal1106 ! create-react-app is a simple way to quickly get an app up and running, and this is for demo purposes only so I don't need to worry about performance or scaling. Next.js, React.js Boilerplate, or whatever build tools you like including building your own from scratch can be used.

🌚 Life is too short to browse without dark mode