DEV Community

Cover image for How to Create a Simple React Calendar with Styled Component
Zhiyue Yi
Zhiyue Yi

Posted on • Edited on

How to Create a Simple React Calendar with Styled Component

Visit my Blog for the original post: How to Create a Simple React Calendar with Styled Component

I found it quite interesting doing small components which are widely used in web developments. When I was a junior web developer, I tended to search libraries or plugins online if I need to build some featured. It could be a hard time to implement it because I didn't try to think how it actually works and I had to rely on the poorly written documents. And sometimes, customization was also difficult because it was hard to understand why the author did the plugin in their way.

The calendar was one of the most common examples. There are a lot of plugins online, but few of them really teach you how it works. When I was in my previous company as a junior developer, I was tasked to customize a calendar with integration of some business requirements, none of the libraries I found online fulfilled my needs. Then I realize, hey, why not build my own calendar from scratch?

It's not hard. Let's do it with React and Styled Component!

Solution

The final implementation can be found at simple-react-calendar if you wish to implement quickly without reading through my explanation.

import * as React from 'react';
import { useState, useEffect } from 'react';
import styled, { css } from 'styled-components';

const Frame = styled.div`
  width: 400px;
  border: 1px solid lightgrey;
  box-shadow: 2px 2px 2px #eee;
`;

const Header = styled.div`
  font-size: 18px;
  font-weight: bold;
  padding: 10px 10px 5px 10px;
  display: flex;
  justify-content: space-between;
  background-color: #f5f6fa;
`;

const Button = styled.div`
  cursor: pointer;
`;

const Body = styled.div`
  width: 100%;
  display: flex;
  flex-wrap: wrap;
`;

const Day = styled.div`
  width: 14.2%;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;

  ${props =>
    props.isToday &&
    css`
      border: 1px solid #eee;
    `}

  ${props =>
    props.isSelected &&
    css`
      background-color: #eee;
    `}
`;

export function Calendar() {
  const DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
  const DAYS_LEAP = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
  const DAYS_OF_THE_WEEK = ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN'];
  const MONTHS = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'];

  const today = new Date();
  const [date, setDate] = useState(today);
  const [day, setDay] = useState(date.getDate());
  const [month, setMonth] = useState(date.getMonth());
  const [year, setYear] = useState(date.getFullYear());
  const [startDay, setStartDay] = useState(getStartDayOfMonth(date));

  useEffect(() => {
    setDay(date.getDate());
    setMonth(date.getMonth());
    setYear(date.getFullYear());
    setStartDay(getStartDayOfMonth(date));
  }, [date]);

  function getStartDayOfMonth(date: Date) {
    return new Date(date.getFullYear(), date.getMonth(), 1).getDay();
  }

  function isLeapYear(year: number) {
    return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
  }

  const days = isLeapYear(date.getFullYear()) ? DAYS_LEAP : DAYS;

  return (
    <Frame>
      <Header>
        <Button onClick={() => setDate(new Date(year, month - 1, day))}>Prev</Button>
        <div>
          {MONTHS[month]} {year}
        </div>
        <Button onClick={() => setDate(new Date(year, month + 1, day))}>Next</Button>
      </Header>
      <Body>
        {DAYS_OF_THE_WEEK.map(d => (
          <Day key={d}>
            <strong>{d}</strong>
          </Day>
        ))}
        {Array(days[month] + (startDay - 1))
          .fill(null)
          .map((_, index) => {
            const d = index - (startDay - 2);
            return (
              <Day
                key={index}
                isToday={d === today.getDate()}
                isSelected={d === day}
                onClick={() => setDate(new Date(year, month, d))}
              >
                {d > 0 ? d : ''}
              </Day>
            );
          })}
      </Body>
    </Frame>
  );
}

Enter fullscreen mode Exit fullscreen mode

Explanation

Initialize Calendar Component

Initialization of the component is rather simple. Firstly, import the necessary libraries, and then, create a function component called Calendar.

Inside of the component, let's return an empty div for now and add in some constants which are

  • DAYS: an array of numbers of days in every month for a normal year
  • DAYS_LEAP: an array of numbers of days in every month for a leap year
  • DAYS_OF_THE_WEEK: an array of names of days of the week
  • MONTHS: an array of names of months
import * as React from 'react';
import { useState, useEffect } from 'react';
import styled, { css } from 'styled-components';

export function Calendar() {
  const DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
  const DAYS_LEAP = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
  const DAYS_OF_THE_WEEK = ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN'];
  const MONTHS = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'];

  // Will be implemented below

  return (
    <div />
  );
}
Enter fullscreen mode Exit fullscreen mode

Identify Component Layout

Now let's decide what the layout of a calendar component is. Since we are building a basic calendar component, we only need a header with a title current month and year, a previous month button and a next month button.

As for the body part, it is consisted of 2 parts, which are one row of days of the week and multiple rows of actual days.

Structure of Calendar Component

Now, let's create these parts using styled components and put them above the calendar function component.

const Frame = styled.div`
  width: 400px;
  border: 1px solid lightgrey;
  box-shadow: 2px 2px 2px #eee;
`;

const Header = styled.div`
  font-size: 18px;
  font-weight: bold;
  padding: 10px 10px 5px 10px;
  display: flex;
  justify-content: space-between;
  background-color: #f5f6fa;
`;

const Button = styled.div`
  cursor: pointer;
`;

const Body = styled.div`
  width: 100%;
  display: flex;
  flex-wrap: wrap;
`;

const Day = styled.div`
  width: 14.2%;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;

  ${props =>
    props.isToday &&
    css`
      border: 1px solid #eee;
    `}

  ${props =>
    props.isSelected &&
    css`
      background-color: #eee;
    `}
`;
Enter fullscreen mode Exit fullscreen mode

Notice that:

  1. I use 14.2% as the width of the Day component, because there can only be 7 days in a week / row and 100% / 7 is approximately 14.2%.
  2. For Day styled component, I will check 2 props isToday and isSelected to show a grey border if the day is today, or a grey background if it is selected

Use React Hooks to Manage Date / Month / Year as States

A calendar must have a current day, month and year displayed. They are considered as states to the component. Hence, we use useState react hook to manage these states. The initial values of them are generated from today's date by default (You may also make the default value of date come from a prop of this component for further extensibility).

Besides current day, month and year, you also need startDay to identify the first day of the month is which day of the week (Monday, Tuesday or others). After you know which day it is, it's much easier for you to identify the positions of all the days in the calendar.

After creating all the states, we also need to manage updates of them. We should make date variable as an entry point for calculations of day, month, year and startDay. Therefore, we can use useEffect react hook to update day, month, year and startDay with a dependency of date, so that later, when we click any day in the calendar, we can call setDate() to update date and trigger the rest of the states to update too.

const today = new Date();
const [date, setDate] = useState(today);
const [day, setDay] = useState(date.getDate());
const [month, setMonth] = useState(date.getMonth());
const [year, setYear] = useState(date.getFullYear());
const [startDay, setStartDay] = useState(calculateStartDayOfMonth(date));

useEffect(() => {
  setDay(date.getDate());
  setMonth(date.getMonth());
  setYear(date.getFullYear());
  setStartDay(calculateStartDayOfMonth(date));
}, [date]);
Enter fullscreen mode Exit fullscreen mode

Get Start Day of the Month

As mentioned above, we need to get the start day of the month, which is rather simple and straightforward.

function getStartDayOfMonth(date: Date) {
  return new Date(date.getFullYear(), date.getMonth(), 1).getDay();
}
Enter fullscreen mode Exit fullscreen mode

Check If It's in a Leap Year

We also needs to check if we are currently in a leap year, so that we can show correct number of days in February.

I extracted a picture from Wikipedia for better illustration of determination of a leap year.

Leap Year

It's quite clear that, if a year is a leap year, the year is divisible by 4 and by 400 but not by 100.

For example,

  • 2020 is a leap year because it is divisible by 4
  • 2010 is not a leap year because it is not divisible by 4
  • 2000 is a leap year because it is divisible by 400
  • 1900 is not a leap year. Although 1900 is divisible by 4 but it is also divisible by 100

(It's better to write a unit test for it!!)

function isLeapYear(year: number) {
  return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
}
Enter fullscreen mode Exit fullscreen mode

Build the Calendar with TSX!

Finally, we can complete the component by adding the render part.

The 2 buttons in the headers trigger setDate() when being clicked. And it will then trigger useEffect() callback and then update day, month, year and startDay, where month and day are displayed in the title of the header, day is used to determine if the day is the current selected day and start day to compute how many empty blocks it should have before the 1st day of the month.

const days = isLeapYear(date.getFullYear()) ? DAYS_LEAP : DAYS;

return (
  <Frame>
    <Header>
      <Button onClick={() => setDate(new Date(year, month - 1, day))}>Prev</Button>
      <div>
        {MONTHS[month]} {year}
      </div>
      <Button onClick={() => setDate(new Date(year, month + 1, day))}>Next</Button>
    </Header>
    <Body>
      {DAYS_OF_THE_WEEK.map(d => (
        <Day key={d}>
          <strong>{d}</strong>
        </Day>
      ))}
      {Array(days[month] + (startDay - 1))
        .fill(null)
        .map((_, index) => {
          const d = index - (startDay - 2);
          return (
            <Day
              key={index}
              isToday={d === today.getDate()}
              isSelected={d === day}
              onClick={() => setDate(new Date(year, month, d))}
            >
              {d > 0 ? d : ''}
            </Day>
          );
        })}
    </Body>
  </Frame>
);
Enter fullscreen mode Exit fullscreen mode

Conclusion

Today I shared how to create a simple react calender with styled component. It's not as difficult as imagined because the only critical part, I think, is to know you need to determine what is the day of the week for the first day of the week. If you can do that, you can determine all the positions of the days. The rest works are just grooming your components to make it more appealing.

Thank you for reading!!

Featured image is credited to Bich Tran from Pexels

Top comments (4)

Collapse
 
johndoe999 profile image
John Doe

Hey great article, helpful.

Suggest a small correction instead of :

const days = isLeapYear ? DAYS_LEAP : DAYS;

it should be :
const days = isLeapYear(date.getFullYear()) ? DAYS_LEAP : DAYS;

isLeapYear never gets called.

Cheers,
Salil

Collapse
 
zhiyueyi profile image
Zhiyue Yi

Yes, you are right! Thank you so much for pointing it out! :D

Collapse
 
romaletodiani profile image
RomanLetodiani

Great article but should not we check month and year also in Day isToday?

Collapse
 
diegopevi05 profile image
Diego Peña Vicente

Hey great article really helpful.