DEV Community

Cover image for Creating a React Calendar Component: Part 3
Elbert Bae
Elbert Bae

Posted on • Edited on

Creating a React Calendar Component: Part 3

Creating an appealing visual component can be an effective way to draw someone towards a part of your web application. In part 1 and part 2 of this series, we explored the logic behind the dates to display and the React code that used a set of dates to render a visible component in the browser. In this next part, we will explore the code that gives our component the visual appeal.

When I first began coding, I never enjoyed CSS as much as the other aspects of development. My first job had a singular CSS file controlling the entire web application's styling. Having limited experience with CSS at the time, this was an absolute nightmare. However, that experience is what drove me to further explore the best way to structure my CSS code moving forward and in future projects. Much like other parts of development, CSS has its own design patterns and practices which were further enabled by technologies such as SASS and Styled Components.

Before we begin, the following examples will be in SASS which is a superset of CSS. SASS is compiled into CSS, but allows functionality which make organizing styling code easier. My 3 favorite aspects of SASS is that it allows nesting of CSS for specificity, creating variables for reusability, and importability of SASS files into each other. Let's do a breakdown of what we'll be taking a look at today:

  1. Styling the calendar header using Flexbox
  2. Styling the month indicators using Flexbox
  3. Styling the weekday indicators using Grid CSS
  4. Styling the date indicators using Grid CSS
  5. Adding in themes

Section 1: Styling the calendar header using Flexbox

Before we begin, let's add some CSS to the main BaeCalendar component so we can see it on the browser and normalize some HTML element styles.

.bae-calendar-container {
  box-sizing: border-box;
  box-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
  height: 100%;
  width: 100%;

  h1,
  h2,
  h3,
  h4,
  h5,
  h6,
  p {
    padding: 0;
    margin: 0;
    line-height: 1;
    font-family: 'Varela Round', sans-serif;
  }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, elements with a class called bae-calendar-container is given a 100% height and width of its parent element. This allows any users importing the component to wrap it in a different container so they can specify the height and width themselves. For now, it will take on the body element's height and width property so we can see it on the browser.

Aside from this, you'll notice that we are taking advantage of SASS's capability of nesting to style the h1, h2, h3... elements to normalize its default styles. Without going into great detail, nesting will translate this into a CSS code that will look like this:

.bae-calendar-container h1, .bae-calendar-container h2 {
  padding: 0;
  margin: 0;
  line-height: 1;
  font-family: 'Varela Round', sans-serif;
}
// And so on...
Enter fullscreen mode Exit fullscreen mode

Header with no styling

Above is the component with no styling that we will transform into the following.

Header with styling

Let's take a moment to look at the HTML layout of the CalendarHeader component.

<div className="bae-calendar-header">
  <div className="left-container">
    <h1>{getReadableWeekday(selectDate)}</h1>
    <h1>{getReadableMonthDate(selectDate)}</h1>
  </div>
  <div className="right-container">
    <h3>{getYear(selectDate)}</h3>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

All of the elements we see here are block elements which by definition is an element that starts a new line. So how do we end up with 2 columns? Thankfully, flexbox is a very useful CSS property we can take advantage of.

.bae-calendar-header {
  display: flex;
}
Enter fullscreen mode Exit fullscreen mode

The display: flex property defines an element to act like a flex element. A flex element acts differently. Essentially, the outer element is still a block element, but any inner elements take on a more fluid form. Unless display: flex is specified, we're not able to apply any other flex based CSS properties.

Here is an example of the component with display: flex.

Header with display flex

Notice how the inner elements are no longer acting as block elements? The container itself wrapping the contents will maintain its block behavior, but this gives us freedom to control the layout of the inner elements. Let's make it so that the readable dates and the years end up on opposite sides by adding justify-content: space-between.

.bae-calendar-header {
  display: flex;
  justify-content: space-between;
}
Enter fullscreen mode Exit fullscreen mode

Self-explanatory right? When flex is in a row format (e.g. left-container and right-container), justify-content modifies the horizontal layout. Keep in mind, that if you are working with a flex element in a column format, justify-content will follow the new change and affect the vertical layout. The option we provide space-between does exactly what the name states. It provides an even spacing between all elements, but no spacing on the edges.

We're making progress, but let's see if we can provide some space to the edges and a border to show where the CalendarHeader component ends.

.bae-calendar-header {
  padding: 15px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
  display: flex;
  justify-content: space-between;
}
Enter fullscreen mode Exit fullscreen mode

Header with padding & shadow

Great! Let's move on using the same flex tricks to our month indicators.


Section 2: Styling the month indicators using Flexbox

Similar to the CalendarHeader component, the MonthIndicator is a div element with 3 block elements inside.

<div className="bae-month-indicator">
  <h4 data-date={monthSet.prev} onClick={changeDate}>
    {monthsFull[getMonth(monthSet.prev)]}
  </h4>
  <h3>{monthsFull[getMonth(monthSet.current)]}</h3>
  <h4 data-date={monthSet.next} onClick={changeDate}>
    {monthsFull[getMonth(monthSet.next)]}
  </h4>
</div>
Enter fullscreen mode Exit fullscreen mode

Here's how it looks with no styling...

Month indicator without styles

And what we want to achieve...

Month indicator with styles

It looks like we can apply similar flex properties so let's add the same things we did to the header.

.bae-month-indicator {
  display: flex;
  justify-content: space-between;
}
Enter fullscreen mode Exit fullscreen mode

It looks pretty good so far. However, let me show you one thing. Although you can't tell, these 3 elements are not perfectly centered. This is a case where it probably doesn't matter too much, but it's fairly simple to ensure that all elements are centered vertically with one simple rule.

.bae-month-indicator {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
Enter fullscreen mode Exit fullscreen mode

The property align-items is similar to justify-content. While the flex is in a row format, while justify-content works horizontally, align-items work vertically. A quick trick to center any item is to provide the parent a display: flex and justify-content: center with align-items: center and you ensure the item is centered inside of the parent element.

Knowledge of Flexbox can be incredibly useful. One of the best ways to learn Flexbox is through an interactive game which you can checkout here!


Section 3: Styling the weekday indicators using Grid CSS

Now that we've taken the time to see how Flexbox is applied to the calendar component, let's spend some time and explore the basics of Grid CSS. Similar to Flexbox, Grid is used to create the layout of the visual components in a web application. There are some differences between Flexbox and Grid, but for the most part, it is possible to achieve a similar layout result with both.

In my experience, I have some to utilise Flexbox and Grid for different scenarios. I use Flexbox primarily for positioning individual elements, but when it comes to repeated and predictable layouts, I prefer to use Grid. Let's explore this as we style the WeekdayIndicator.

Referring back to part 2 of the series, here is how the WeekdayIndicator looks as React code:

const weekdays ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];

const WeekdayIndicator = () => {
  const weekdayIcons = weekdays.map((day, key) => {
    return (
      <div className="weekday-indicator-icon" key={key}>
        {day}
      </div>
    );
  });
  return <div className="bae-weekday-indicators">{weekdayIcons}</div>;
};
Enter fullscreen mode Exit fullscreen mode

Which translates to the following HTML:

<div className="bae-weekday-indicators">
  <div className="weekday-indicator-icon">
    Sun
  </div>
  <div className="weekday-indicator-icon">
    Mon
  </div>
  <div className="weekday-indicator-icon">
    Tue
  </div>
  <div className="weekday-indicator-icon">
    Wed
  </div>
  <div className="weekday-indicator-icon">
    Thu
  </div>
  <div className="weekday-indicator-icon">
    Fri
  </div>
  <div className="weekday-indicator-icon">
    Sat
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Without any styling, here is what these elements look like with their natural block behaviors.

Weekday indicators with no styling

And here is what we want it to look like after styling it.

Weekday indicators styled

While Flexbox can certainly be used to achieve this outcome, we are going to use a combination of Grid to get the weekdays horizontal and evenly distributed while using Flexbox to center the text inside of each div.weekday-indicator-icon elements.

Let's start with the Grid using two properties called grid-template-columns and grid-template-rows.

.bae-weekday-indicators {
  display: grid;
  grid-template-columns: repeat(7, minmax(auto, 1fr));
  grid-template-rows: 1;
  padding: 15px // To keep padding consistent with the other components so far
}
Enter fullscreen mode Exit fullscreen mode

In grid-template-columns, the value repeat(7, minmax(auto, 1fr)) is saying create 7 columns with inner elements with its minimum size to auto (change to fit) and maximum size to 1fr (1/7 of the total container size). As an example, you could make it have only 3 columns whereby it will override the grid-template-rows by overflowing and creating new rows to fit the number of inner elements inside of the parent container resulting in this.

Weekday overflow grid example

Let's switch it back and move on. As you can see, the weekday indicator icons are not centered which we'll work on next by using the Flexbox concepts we used above. Since the grid lays out each of the containers wrapping the weekday texts, we need to add some styling to elements with the class name weekday-indicator-icon.

.bae-weekday-indicators {
  display: grid;
  grid-template-columns: repeat(7, minmax(auto, 1fr));
  grid-template-rows: 1;
  padding: 15px;
  .weekday-indicator-icon {
    height: 25px;
    width: 25px;
    display: flex;
    justify-self: center;
    justify-content: center;
    align-items: center;
    padding: 5px;
    font-weight: bold;
    cursor: pointer;
  }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, I applied the Flexbox trick of centering using justify-content: center and align-items: center. This ensures that the elements internal to the div.weekday-indicator-icon are centered. But, what is justify-self doing? In this case, we're applying a horizontal CSS property, but to the element by referring to itself. Here is an example of how the elements look without justify-self: center and below that, with it.

Weekday with no justify self

Weekday with justify self


Section 4: Styling the date indicators using Grid CSS

Now that we have the grid example above with the WeekdayIndicator component, let's apply those same changes to the DateIndicator to achieve this.

Date styled

As a quick refresher, here's the DateIndicator component.

const DateIndicator = ({ activeDates, selectDate, setSelectDate }) => {
  const changeDate = (e) => {
    setSelectDate(e.target.getAttribute('data-date'));
  };

  const datesInMonth = getDatesInMonthDisplay(
    getMonth(selectDate) + 1,
    getYear(selectDate)
  );

  const monthDates = datesInMonth.map((i, key) => {
    const selected = getMonthDayYear(selectDate) === getMonthDayYear(i.date) ? 'selected' : '';

    return (
      <div
        className=`date-icon ${selected}`
        data-active-month={i.currentMonth}
        data-date={i.date.toString()}
        key={key}
        onClick={changeDate}
      >
        {getDayOfMonth(i.date)}
      </div>
    );
  });

  return <div className="bae-date-indicator">{monthDates}</div>;
};
Enter fullscreen mode Exit fullscreen mode

There are two things we want to do here. One is setup the grid using the same principles above using Grid CSS. The other, is visually distinguishing between the active dates in the current month, versus the overflow dates of the previous and following months. The grid part should be simple, so here's what we'll add.

.bae-date-indicator {
  display: grid;
  grid-template-columns: repeat(7, minmax(auto, 1fr));
  grid-template-rows: repeat(6, minmax(35px, 1fr));
  grid-gap: 5px;
  padding: 0 15px;
  .date-icon {
    display: flex;
    justify-content: center;
    justify-self: center;
    align-items: center;
    height: 25px;
    width: 25px;
    padding: 5px;
    cursor: pointer;
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, we want to fade out the overflow dates. Looking at the component code, we can see that the data attribute data-active-month={i.currentMonth} is applied. This can either be data-active-month="true" or data-active-month="false", providing us with a simple way to identify what is the current month or not in our CSS with the following change to target elements with the specificity .bae-date-indicator .date-icon[data-active-month="false"].

.bae-date-indicator {
  display: grid;
  grid-template-columns: repeat(7, minmax(auto, 1fr));
  grid-template-rows: repeat(6, minmax(35px, 1fr));
  grid-gap: 5px;
  padding: 0 15px;
  .date-icon {
    display: flex;
    justify-content: center;
    justify-self: center;
    align-items: center;
    height: 25px;
    width: 25px;
    padding: 5px;
    cursor: pointer;
    &[data-active-month='false'] {
      color: rgba(0, 0, 0, 0.3);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

That's it! The last thing we're going to add is the styling for elements with the selected class name which we will use to improve our UI by making it clear to our users which date is currently selected on the calendar.

.bae-date-indicator {
  display: grid;
  grid-template-columns: repeat(7, minmax(auto, 1fr));
  grid-template-rows: repeat(6, minmax(35px, 1fr));
  grid-gap: 5px;
  padding: 0 15px;
  .date-icon {
    display: flex;
    justify-content: center;
    justify-self: center;
    align-items: center;
    height: 25px;
    width: 25px;
    padding: 5px;
    cursor: pointer;
    &[data-active-month='false'] {
      color: rgba(0, 0, 0, 0.3);
    }
    &.selected {
      border-radius: 50%;
      box-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

At this point, putting our components together with styling should look like this.

Full component with no theme


Section 5: Adding in themes

Now that we have the layout in place for our component, let's add a little bit of life to it by adding colors. Color matching and design is definitely not my strong suite which is why I want to make this as easy to modify as possible. To apply our themes, we're going to take advantage of CSS specificity rules by using nesting in SASS. Here is our root bae-calendar.sass file.

.bae-calendar-container {
  box-sizing: border-box;
  box-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
  height: 100%;
  width: 100%;

  h1,
  h2,
  h3,
  h4,
  h5,
  h6,
  p {
    padding: 0;
    margin: 0;
    line-height: 1;
    font-family: 'Varela Round', sans-serif;
  }
  &.salmon-theme {
    @import './themes/salmon.sass';
  }

  @import './components/calendar-header.sass';
  @import './components/weekday-indicator.sass';
  @import './components/date-indicator.sass';
}
Enter fullscreen mode Exit fullscreen mode

As you can see, this root SASS file imports multiple SASS files consisting of the styling for each of our individual sub-components that we styled above. You also see that there is a new file under a class name salmon-theme imported with the & selector to apply styles on elements with .bar-calendar-container.salmon-theme. Here is where we will outline the theme styles for our calendar component.

Inside of our salmon.sass file, we have the following:

$primaryColor: #fa8072;
$secondaryColor: #ffa98f;
$highlightTextColor: #d95e39;
$activeTextColor: #f8f8ff;

h1,
h2,
h3,
h4,
h5,
h6 {
  color: $activeTextColor;
}

.bae-calendar-header {
  background-color: $primaryColor;
}

.bae-weekday-indicators {
  .weekday-indicator-icon {
    color: $highlightTextColor;
    &.active {
      background-color: $primaryColor;
      color: $activeTextColor;
    }
  }
}

.bae-date-indicator {
  .date-icon {
    &.active {
      background-color: $secondaryColor;
      color: $activeTextColor;
    }
    &.selected {
      background-color: $primaryColor;
      color: $activeTextColor;
    }
  }
}

.bae-month-indicator {
  background-color: $primaryColor;
}
Enter fullscreen mode Exit fullscreen mode

Without going into much detail, can you see how we utilise SASS variable names and the organization of the theme file? Easy to understand right? All of our texts will apply the color: $activeTextColor and we see some other changes in the background-color properties as well. Here's how the component looks with this style.

Component fully styled

Easy right? Can you see how we can take advantage of the way this has been organized? All we need to do now is select the colors we want for the following variables:

$primaryColor: #fa8072;
$secondaryColor: #ffa98f;
$highlightTextColor: #d95e39;
$activeTextColor: #f8f8ff;
Enter fullscreen mode Exit fullscreen mode

For example, monochrome-theme uses the following variables.

$primaryColor: #646e78;
$secondaryColor: #8d98a7;
$highlightTextColor: #6d7c90;
$activeTextColor: #f8f8ff;
Enter fullscreen mode Exit fullscreen mode

Once we change these variables, we can copy it to a new file and import it again like this.

.bae-calendar-container {
  box-sizing: border-box;
  box-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
  height: 100%;
  width: 100%;

  h1,
  h2,
  h3,
  h4,
  h5,
  h6,
  p {
    padding: 0;
    margin: 0;
    line-height: 1;
    font-family: 'Varela Round', sans-serif;
  }
  &.salmon-theme {
    @import './themes/salmon.scss';
  }
  &.monochrome-theme {
    @import './themes/monochrome.scss';
  }

  @import './components/calendar-header.scss';
  @import './components/weekday-indicator.scss';
  @import './components/date-indicator.scss';
}
Enter fullscreen mode Exit fullscreen mode

Monochrome styled

To make things easier for our users, we can create preset themes and allow them to use the components by specifying which class names to append to the main element. As an example, our calendar component can do something like this to accept a prop called theme which translates to a specific className.

import React, { useState } from 'react';
import { getToday } from './utils/moment-utils';
import './bae-calendar.scss';

import CalendarHeader from './components/calendar-header';
import WeekdayIndicator from './components/weekday-indicator';
import DateIndicator from './components/date-indicator';
import MonthIndicator from './components/month-indicator';
// https://uicookies.com/html-calendar/

import { presetDateTracker } from './utils/date-utils';

// preset themes that are available
const themes = {
  salmon: 'salmon-theme',
  monochrome: 'monochrome-theme',
  rouge: 'rouge-theme',
};

const BaeCalendar = ({ theme, activeDates, onDateSelect }) => {
  const presetActiveDates = useRef(presetDateTracker(activeDates || []));
  const [selectDate, setSelectDate] = useState(getToday());

  // Add the theme name to the main container `${themes[theme]}`
  return (
    <div className={`bae-calendar-container ${themes[theme]}`}>
      <CalendarHeader selectDate={selectDate} />
      <WeekdayIndicator />
      <DateIndicator
        activeDates={presetActiveDates.current}
        selectDate={selectDate}
        setSelectDate={setSelectDate}
      />
      <MonthIndicator selectDate={selectDate} setSelectDate={setSelectDate} />
    </div>
  );
};

export default BaeCalendar;
Enter fullscreen mode Exit fullscreen mode

This can then be imported as such...

<BaeCalendar
  theme="monochrome"
/>
Enter fullscreen mode Exit fullscreen mode

And that's it! Hope this information provide some insight in how you can organize the styling for your components. Style files require just as much organization and design as other areas of code so make sure you don't skip out on careful planning when you are creating your next front-end projects. In the next part, I'll show you how the component files were structured and add a few features that will allow our component users to get the date they select back to use within their own programs as well as pass in pre-selected dates.

Top comments (0)