DEV Community šŸ‘©ā€šŸ’»šŸ‘Øā€šŸ’»

DEV Community šŸ‘©ā€šŸ’»šŸ‘Øā€šŸ’» is a community of 967,611 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Create account Log in
Cover image for How to Build a Custom Calendar/Date picker Component in React
Francisco Mendes
Francisco Mendes

Posted on • Updated on

How to Build a Custom Calendar/Date picker Component in React

Introduction

Many web applications need to manage dates using a calendar, however the overwhelming majority of articles/tutorials always use third-party libraries. It's not that it's bad, on the contrary, it helps a lot to prototype an application, but if the design and requirements are custom, it's very difficult for the developer.

For that same reason in today's article I will teach you how to create a base component that can later be extended to add even more functionality.

What are we going to use?

Today to be different we are going to use two of my favorite libraries:

  • Day.js - is a library that helps us manipulate, parse and validate dates
  • Stitches - a css-in-js styling library with phenomenal development experience

Bear in mind that although these are the libraries used in this article, the same result is also easily replicable with others.

Prerequisites

To follow this tutorial, you need:

  • Basic understanding of React
  • Basic understanding of TypeScript

If you're not familiar with TypeScript, it's okay because you just "ignore" the data-types and the code is exactly the same as in JavaScript.

Getting Started

As a first step, create a project directory and navigate into it:

yarn create vite react-calendar-ts --template react-ts
cd react-calendar-ts
Enter fullscreen mode Exit fullscreen mode

Now we can install the necessary dependencies:

yarn add dayjs react-icons @stitches/react @fontsource/anek-telugu
Enter fullscreen mode Exit fullscreen mode

Then we create the styles of our html elements in a file called styles.ts:

// @/src/styles.ts
import { styled } from "@stitches/react";

export const MainWrapper = styled("div", {
  width: 240,
  borderRadius: 10,
  padding: 20,
  backgroundColor: "white",
  boxShadow: "-6px 7px 54px -24px rgba(0,0,0,0.5)",
  fontFamily: "Anek Telugu",
});

export const CalendarHeaderWrapper = styled("div", {
  display: "flex",
  alignItems: "center",
  justifyContent: "space-between",
});

export const WeekDaysWrapper = styled("div", {
  display: "flex",
  flexDirection: "row",
  justifyContent: "space-between",
});

export const WeekDayCell = styled("div", {
  height: 30,
  width: 30,
  margin: 2,
  display: "flex",
  alignItems: "center",
  justifyContent: "center",
  color: "#9BA4B4",
});

export const CalendarContentWrapper = styled("div", {
  display: "flex",
  flexDirection: "row",
});

export const CalendarDayCell = styled("div", {
  height: 30,
  width: 30,
  display: 'flex',
  alignItems: "center",
  justifyContent: "center",
  borderRadius: 6,
  margin: 2,

  variants: {
    variant: {
      default: {
        color: "#1B1B2F",
      },
      today: {
        color: "#E43F5A",
      },
      nextMonth: {
        color: "#DAE1E7",
      },
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

With the styling of the elements created and the variants of the day already added, we can now start working on the App.tsx component that will contain all the logic of today's article:

// @/src/App.tsx
import "@fontsource/anek-telugu";
import { useCallback, useMemo, useState } from "react";
import dayjs, { Dayjs } from "dayjs";
import { MdKeyboardArrowLeft, MdKeyboardArrowRight } from "react-icons/md";

import * as Styles from "./styles";

export const App = () => {
  // logic goes here...
  return (
    // JSX goes here...
  )
}
Enter fullscreen mode Exit fullscreen mode

In the code above we made the necessary imports to create our component. The next step is to acquire the values of three important things, the current day, the first day of the month and what the first day of the first week of the month is.

// @/src/App.tsx
import "@fontsource/anek-telugu";
import { useCallback, useMemo, useState } from "react";
import dayjs, { Dayjs } from "dayjs";
import { MdKeyboardArrowLeft, MdKeyboardArrowRight } from "react-icons/md";

import * as Styles from "./styles";

export const App = () => {
  const [selectedDate, setSelectedDate] = useState<Dayjs>(dayjs());

  const currentDay = useMemo(() => dayjs().toDate(), []);

  const firstDayOfTheMonth = useMemo(
    () => selectedDate.clone().startOf("month"),
    [selectedDate]
  );

  const firstDayOfFirstWeekOfMonth = useMemo(
    () => dayjs(firstDayOfTheMonth).startOf("week"),
    [firstDayOfTheMonth]
  );


  // more logic goes here...
  return (
    // JSX goes here...
  )
}
Enter fullscreen mode Exit fullscreen mode

With these three dates acquired, we now need to create two functions, one to generate the first day of each week of the month, while the second function will be responsible for generating the week of the month taking into account the first day of the week.

// @/src/App.tsx
import "@fontsource/anek-telugu";
import { useCallback, useMemo, useState } from "react";
import dayjs, { Dayjs } from "dayjs";
import { MdKeyboardArrowLeft, MdKeyboardArrowRight } from "react-icons/md";

import * as Styles from "./styles";

export const App = () => {
  const [selectedDate, setSelectedDate] = useState<Dayjs>(dayjs());

  const currentDay = useMemo(() => dayjs().toDate(), []);

  const firstDayOfTheMonth = useMemo(
    () => selectedDate.clone().startOf("month"),
    [selectedDate]
  );

  const firstDayOfFirstWeekOfMonth = useMemo(
    () => dayjs(firstDayOfTheMonth).startOf("week"),
    [firstDayOfTheMonth]
  );

  const generateFirstDayOfEachWeek = useCallback((day: Dayjs): Dayjs[] => {
    const dates: Dayjs[] = [day];
    for (let i = 1; i < 6; i++) {
      const date = day.clone().add(i, "week");
      dates.push(date);
    }
    return dates;
  }, []);

  const generateWeek = useCallback((day: Dayjs): Date[] => {
    const dates: Date[] = [];
    for (let i = 0; i < 7; i++) {
      const date = day.clone().add(i, "day").toDate();
      dates.push(date);
    }
    return dates;
  }, []);


  // more logic goes here...
  return (
    // JSX goes here...
  )
}
Enter fullscreen mode Exit fullscreen mode

Now that we have the necessary dates and the necessary functions to generate the weeks of the month, we now need to use the useMemo() hook to react to the changes of the dates mentioned above and generate the days and weeks of the month, as well as memoizing the same, as follows:

// @/src/App.tsx
import "@fontsource/anek-telugu";
import { useCallback, useMemo, useState } from "react";
import dayjs, { Dayjs } from "dayjs";
import { MdKeyboardArrowLeft, MdKeyboardArrowRight } from "react-icons/md";

import * as Styles from "./styles";

export const App = () => {
  const [selectedDate, setSelectedDate] = useState<Dayjs>(dayjs());

  const currentDay = useMemo(() => dayjs().toDate(), []);

  const firstDayOfTheMonth = useMemo(
    () => selectedDate.clone().startOf("month"),
    [selectedDate]
  );

  const firstDayOfFirstWeekOfMonth = useMemo(
    () => dayjs(firstDayOfTheMonth).startOf("week"),
    [firstDayOfTheMonth]
  );

  const generateFirstDayOfEachWeek = useCallback((day: Dayjs): Dayjs[] => {
    const dates: Dayjs[] = [day];
    for (let i = 1; i < 6; i++) {
      const date = day.clone().add(i, "week");
      dates.push(date);
    }
    return dates;
  }, []);

  const generateWeek = useCallback((day: Dayjs): Date[] => {
    const dates: Date[] = [];
    for (let i = 0; i < 7; i++) {
      const date = day.clone().add(i, "day").toDate();
      dates.push(date);
    }
    return dates;
  }, []);


  const generateWeeksOfTheMonth = useMemo((): Date[][] => {
    const firstDayOfEachWeek = generateFirstDayOfEachWeek(
      firstDayOfFirstWeekOfMonth
    );
    return firstDayOfEachWeek.map((date) => generateWeek(date));
  }, [generateFirstDayOfEachWeek, firstDayOfFirstWeekOfMonth, generateWeek]);

  return (
    // JSX goes here...
  )
}
Enter fullscreen mode Exit fullscreen mode

With the logic finished, we just need to map the data we get in the generateWeeksOfTheMonth variable, but we also have to take into account that we need to navigate between months and update the selected date. Just like we need to visually identify the current day on the calendar. Which can be done as follows:

// @/src/App.tsx
import "@fontsource/anek-telugu";
import { useCallback, useMemo, useState } from "react";
import dayjs, { Dayjs } from "dayjs";
import { MdKeyboardArrowLeft, MdKeyboardArrowRight } from "react-icons/md";

import * as Styles from "./styles";

export const App = () => {
  // hidden for simplicity...

  return (
    <Styles.MainWrapper>
      <Styles.CalendarHeaderWrapper>
        <h3>{selectedDate.clone().format("MMM YYYY")}</h3>
        <div>
          <MdKeyboardArrowLeft
            size={25}
            onClick={() => setSelectedDate((date) => date.subtract(1, "month"))}
          />
          <MdKeyboardArrowRight
            size={25}
            onClick={() => setSelectedDate((date) => date.add(1, "month"))}
          />
        </div>
      </Styles.CalendarHeaderWrapper>
      <Styles.WeekDaysWrapper>
        {generateWeeksOfTheMonth[0].map((day, index) => (
          <Styles.WeekDayCell key={`week-day-${index}`}>
            {dayjs(day).format("dd")}
          </Styles.WeekDayCell>
        ))}
      </Styles.WeekDaysWrapper>
      {generateWeeksOfTheMonth.map((week, weekIndex) => (
        <Styles.CalendarContentWrapper key={`week-${weekIndex}`}>
          {week.map((day, dayIndex) => (
            <Styles.CalendarDayCell
              key={`day-${dayIndex}`}
              variant={
                selectedDate.clone().toDate().getMonth() !== day.getMonth()
                  ? "nextMonth"
                  : dayjs(currentDay).isSame(day, "date")
                  ? "today"
                  : "default"
              }
            >
              {day.getDate()}
            </Styles.CalendarDayCell>
          ))}
        </Styles.CalendarContentWrapper>
      ))}
    </Styles.MainWrapper>
  )
}
Enter fullscreen mode Exit fullscreen mode

If you've followed the article so far, I believe you should have a result very similar to this one in your browser:

gif

What are the next challenges?

This point is quite relative, but I would focus on the following features:

  • create a component for the calendar that is reusable
  • the component must be stateless
  • single-day and multi-day selection
  • in the selection of multiple days, have a toggle whether to include weekends or not

Conclusion

As usual, I hope you enjoyed the article and that it helped you with an existing project or simply wanted to try it out.

If you found a mistake in the article, please let me know in the comments so I can correct it. Before finishing, if you want to access the source code of this article, I leave here the link to the github repository.

Top comments (0)

Take a look at this:

Settings

Go to your customization settings to nudge your home feed to show content more relevant to your developer experience level. šŸ›