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:
- Styling the calendar header using Flexbox
- Styling the month indicators using Flexbox
- Styling the weekday indicators using Grid CSS
- Styling the date indicators using Grid CSS
- 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;
}
}
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...
Above is the component with no styling that we will transform into the following.
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>
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;
}
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
.
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;
}
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;
}
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>
Here's how it looks with no styling...
And what we want to achieve...
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;
}
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;
}
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>;
};
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>
Without any styling, here is what these elements look like with their natural block
behaviors.
And here is what we want it to look like after styling it.
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
}
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.
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;
}
}
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.
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.
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>;
};
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;
}
}
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);
}
}
}
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);
}
}
}
At this point, putting our components together with styling should look like this.
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';
}
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;
}
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.
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;
For example, monochrome-theme
uses the following variables.
$primaryColor: #646e78;
$secondaryColor: #8d98a7;
$highlightTextColor: #6d7c90;
$activeTextColor: #f8f8ff;
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';
}
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;
This can then be imported as such...
<BaeCalendar
theme="monochrome"
/>
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)