How many times have you searched for that one specific library that meets your needs? How much time have you spent customizing it to fit your project's requirements? I must admit, waaay too much. That's why I am pleasantly surprised, how many things are much simpler than they initially seem.
Today, we will implement together the main functionality of a simple calendar, which can serve as the foundation for a date picker or a visualization of events spread over time.
Although we will use Svelte to build it, the main logic remains the same regardless of the tools you choose to use.
Main skeleton
Let's start by building a simple calendar layout. For this purpose, we will use TailwindCSS, which allows us to quickly and easily style our component. Nothing complicated: a white box, a list of day names at the top, and buttons with dates at bottom. Everything will be spaced out evenly using the grid layout.
<script>
const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
let dates = [];
</script>
<div class="bg-white flex flex-col border border-gray-300 p-2 rounded shadow">
<div class="grid grid-cols-7 gap-2 border-b border-gray-300">
{#each days as day}
<div class="font-semibold text-center p-1">{day}</div>
{/each}
</div>
<div class="grid grid-cols-7 gap-2 mt-2">
{#each dates as date}
<button
on:click={() => console.log(date.date)}
class="flex justify-center items-center"
>
{date.day}
</button>
{/each}
</div>
</div>
We don't have any dates yet, so the calendar looks a little empty.
The heart of the calendar
The first thing we need to know is how many days a given month has and which day it starts. Thankfully, JavaScript has us covered. For that purpose, we will use the built-in Date
object.
const today = new Date();
// Let's get the current month and year
// We will use these variables for keeping track of the currently displayed month
let month = today.getMonth();
let year = today.getFullYear();
The Date
object constructor receives a year, month, and day as its arguments (actually, the list goes to milliseconds, but we won't need that), but the most important part is how it behaves when we overflow (or underflow) one of its arguments. If we, for example, try to create a date with 13 months, it will automatically adjust the year and month for us. The code may look weird if you don't know about it, but it's a very handy feature.
// Let's get the last day of the previous month by setting the day to 0
// As we all know, each month starts on day 1, so 0 is the day before
let prevMonth = new Date(year, month, 0)
// Knowing the day, we can check which day of the week it was
// We will use the getDay(), which returns a number from 0 to 6, where 0 is Sunday
// From now on, we will refere to the day of the week as an day index
let prevMontyDayIndex = prevMonth.getDay();
// That's actually everything we need to know to populate our calendar
// First, let's add empty values to shift the first day.
for (let i = 1; i <= prevMontyDayIndex; i++) {
// You can see that we started from 1 instead 0
// If the previous month ended on Sunday, we don't need to shift anything
dates.push({
day: ""
});
}
// Then let's add the current month
let currentMonthSize = new Date(year, month + 1, 0).getDate();
// Yep, that's a little weird way of getting the last day of the current month
// but it is how it is in JS
for (let i = 1; i <= currentMonthSize; i++) {
// Let's create a new date object which we can use later (in our case for console.log)
const date = new Date(year, month, i)
dates.push({
date,
day: i,
});
}
And... that's it! You could actually stop here and have a working calendar. But let's make it a little fancier.
Displaying Adjacent Days
Let's wrap the days population logic in a function. This will help with code organization and will allow us to easily switch between months.
const setDays = () => {
let newDates = [];
let prevMonth = new Date(year, month, 0);
let prevMonthDayIndex = prevMonth.getDay();
// Now we need to know the actual previous month's days to display them
// For that, we will subtract the day index from the last day number
// The logic behind it is quite simple:
// If the last day was Tuesday and we always start a week from Monday, then
// there are two days to display
// If the previous month had 30 days, we would only show the 29th and 30th
// Since we do not need to display the previous month if the current starts on Monday
// we do not need to worry about the last day being Sunday (index 0)
let calendarStartDate = prevMonth.getDate() - prevMonthDayIndex;
let currentMonthSize = new Date(year, month + 1, 0).getDate();
for (let i = 1; i <= prevMonthDayIndex; i++) {
// To get current day we just add the index to the calendar beginning
const day = calendarStartDate + i;
const date = new Date(year, month - 1, day);
newDates.push({
date,
day,
// Additionally, we will add a flag to mark today
// getTime and setHours functions return absolute time in milliseconds since 1970
today: date.getTime() == today.setHours(0, 0, 0, 0),
});
}
// For the current month, we only add a simple flag to visually distinguish it
for (let i = 1; i <= currentMonthSize; i++) {
const date = new Date(year, month, i);
newDates.push({
date,
day: i,
current: true,
today: date.getTime() == today.setHours(0, 0, 0, 0),
});
}
// And let's display the next month
// First, let's check if we even need to do that
// If the total number of days in the calendar is divisible by 7, it means
// that the last day of the month is Sunday and we do not need to do anything
if (newDates.length % 7) {
// Let's get the index of the last day of the current month
let lastDayIndex = new Date(year, month + 1, 0).getDay();
// And we just fill the rest of the calendar with missing days to a full week
for (let i = 1; i <= 7 - lastDayIndex; i++) {
const date = new Date(year, month + 1, i);
newDates.push({
date,
day: i,
today: date.getTime() == today.setHours(0, 0, 0, 0),
});
}
}
// At the end, we just need to trigger Svelte reactivity
dates = newDates;
};
Let's also update the way we display the dates in our component.
<button
on:click={() => console.log(date.date)}
class:text-gray-400={!date.current}
class:font-semibold={date.today}
class="flex justify-center items-center"
>
{date.day}
</button>
Switching Months
Switching the current month will be as simple as changing the month
and year
variable values. We don't need to do any calculations ourselves, as the Date object constructor and its "overflow" will handle it for us.
const prevMonth = () => {
const newDate = new Date(year, month - 1);
year = newDate.getFullYear();
month = newDate.getMonth();
setDays()
}
const nextMonth = () => {
const newDate = new Date(year, month + 1);
year = newDate.getFullYear();
month = newDate.getMonth();
setDays()
}
Let's also adjust our view a little. We will need a list of months to display and buttons to switch between them.
const months = [
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
];
<div class="flex justify-between p-1">
<button on:click={prevMonth}>Prev</button>
<div class="font-bold">
{months[month]} / {year}
</div>
<button on:click={nextMonth}>Next</button>
</div>
And that’s it! We have a core calendar functionality that can be used as a base for more advanced features. Since you have full control over calendar rendering, you can easily adapt it to display any information you need and handle input in any way you want. For certain use cases, you may want to construct its HTML in a different way, but now you have full knowledge about the logic behind it. Have fun!
At the end, let’s put everything together and add the missing Svelte parts. If you want to use code from this tutorial, this is the part you should copy.
Putting it all together
<script>
import { onMount } from "svelte";
const months = [
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
];
const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
let dates = [];
const today = new Date();
let month = today.getMonth();
let year = today.getFullYear();
const setDays = () => {
let newDates = [];
let prevMonth = new Date(year, month, 0)
let prevMontyDayIndex = prevMonth.getDay();
let calendarStartDate = prevMonth.getDate() - prevMontyDayIndex;
let currentMonthSize = new Date(year, month + 1, 0).getDate();
for (let i = 1; i <= prevMontyDayIndex; i++) {
const day = calendarStartDate + i
const date = new Date(year, month - 1, day)
newDates.push({
date,
day,
today: date.getTime() == today.setHours(0, 0, 0, 0),
});
}
for (let i = 1; i <= currentMonthSize; i++) {
const date = new Date(year, month, i)
newDates.push({
date,
day: i,
current: true,
today: date.getTime() == today.setHours(0, 0, 0, 0),
});
}
if (newDates.length % 7) {
let lastDayIndex = new Date(year, month + 1, 0).getDay();
for (let i = 1; i <= 7 - lastDayIndex; i++) {
const date = new Date(year, month + 1, i)
newDates.push({
date,
day: i,
today: date.getTime() == today.setHours(0, 0, 0, 0),
});
}
}
dates = newDates;
}
const prevMonth = () => {
const newDate = new Date(year, month - 1);
year = newDate.getFullYear();
month = newDate.getMonth();
setDays()
}
const nextMonth = () => {
const newDate = new Date(year, month + 1);
year = newDate.getFullYear();
month = newDate.getMonth();
setDays()
}
onMount(() => {
setDays();
});
</script>
<div class="bg-white flex flex-col border border-gray-300 p-2 rounded shadow">
<div class="flex justify-between p-1">
<button on:click={prevMonth}>Prev</button>
<div class="font-bold">
{months[month]} / {year}
</div>
<button on:click={nextMonth}>Next</button>
</div>
<div class="grid grid-cols-7 gap-2 border-y border-gray-300">
{#each days as day}
<div class="font-semibold text-center p-1">{day}</div>
{/each}
</div>
<div class="grid grid-cols-7 gap-2 mt-2">
{#each dates as date}
<button
on:click={() => console.log(date.date)}
class:text-gray-400={!date.current}
class:font-semibold={date.today}
class="flex justify-center items-center"
>
{date.day}
</button>
{/each}
</div>
</div>