There sure are a lot of date pickers on the Internet for jQuery, Bootstrap, React, and whatever else. But I understand, it really does not make any sense to load an entire library, just for the sake of a simple plugin. So here it is, a sharing of my simple lightweight date picker, all done in pure CSS Javascript – Read on!
ⓘ I have included a zip file with all the source code at the start of this tutorial, so you don’t have to copy-paste everything… Or if you just want to dive straight in.
TABLE OF CONTENTS
DOWNLOAD & DEMO
Here is the download link to the script, and a short section for you guys who just want to use the script quickly in your own project.
QUICK NOTES
If you spot a bug, feel free to comment below. I try to answer short questions too, but it is one person versus the entire world… If you need answers urgently, please check out my list of websites to get help with programming.
SOURCE CODE DOWNLOAD
Click here to download the source code, I have released it under the MIT license, so feel free to build on top of it or use it in your own project.
EXAMPLE 1) POPUP DATE PICKER
<!-- (A) LOAD DATE PICKER -->
<link href="dp-light.css" rel="stylesheet">
<script defer src="datepicker.js"></script>
<!-- (B) THE HTML -->
<input type="text" id="input-pop" placeholder="Popup"/>
<!-- (C) ATTACH DATE PICKER ON LOAD -->
<script>
window.addEventListener("load", () => {
picker.attach({ target: "input-pop" });
});
</script>
This is the simplest way to use the date picker.
- Load the date picker CSS and Javascript. Doh.
- Define the HTML
<input>
field, give it anid
. - Use
picker.attach()
to attach the date picker – Just specify thetarget
HTML field (id
of the HTML field).
EXAMPLE 2) INLINE DATE PICKER
<!-- (B) THE HTML -->
<input type="text" id="input-inline" placeholder="Inline"/>
<div id="pick-inline"></div>
<!-- (C) ATTACH DATE PICKER ON LOAD -->
<script>
window.addEventListener("load", () => {
picker.attach({
target: "input-inline",
container: "pick-inline"
});
});
</script>
Pretty much the same, but we now also define a <div>
to spawn the date picker inline.
EXAMPLE 3) ALL DATE PICKER OPTIONS
<!-- (B) THE HTML -->
input type="text" id="input-opt" placeholder="Options"/>
<div id="pick-opt"></div>
<!-- (C) ATTACH DATE PICKER ON LOAD -->
<script>
window.addEventListener("load", () => {
picker.attach({
target: "input-opt",
container: "pick-opt",
disableday : [2, 7],
startmon: true,
yrange: 5,
onpick: () => { alert("PICKED"); }
});
});
</script>
The date picker also takes in a couple of options:
disableday
An array to disable certain days. For example,[2, 7]
will disable Tuesdays and Sundays.startmon
To set if the week starts on a Monday. By default, this is false and the week starts on Sunday.yrange
The selectable year range. This defaults to +/- 10 years.onpick
Function to run upon picking a date.
HOW IT WORKS
In this section, we will walk through the mechanics of how the date picker works – This script is open source, so feel free to modify it as you please.
PART A) ATTACH DATE PICKER
// (A) ATTACH DATEPICKER TO TARGET
// target: field to populate
// container: generate datepicker in here (for inline datepicker)
// startmon: start on mon? (optional, default false)
// yrange: year select range (optional, default 10)
// disableday: days to disable, e.g. [2,7] to disable tue and sun (optional)
// onpick : function to call on select date (optional)
instances : [],
attach : (opt) => {
// (A1) SET DEFAULT OPTIONS & REGISTER INSTANCE
opt.target = document.getElementById(opt.target);
opt.target.readOnly = true; // PREVENT ONSCREEN KEYBOARD
if (opt.container) { opt.container = document.getElementById(opt.container); }
opt.startmon = opt.startmon ? true : false;
opt.yrange = opt.yrange ? opt.yrange : 10;
const id = picker.instances.length;
picker.instances.push(opt);
let inst = picker.instances[id];
// (A2) TEMP VARS + CURRENT MONTH YEAR (UTC+0)
let months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
temp, today = new Date(),
thisMonth = today.getUTCMonth(), // JAN IS 0
thisYear = today.getUTCFullYear();
// (A3) GENERATE HTML
// (A3-1) HTML DATEPICKER WRAPPER
inst.hPick = document.createElement("div");
inst.hPick.classList.add("picker");
// (A3-2) HTML MONTH SELECT
inst.hMonth = document.createElement("select");
inst.hMonth.classList.add("picker-m");
for (let m in months) {
temp = document.createElement("option");
temp.value = +m + 1;
temp.text = months[m];
inst.hMonth.appendChild(temp);
}
inst.hMonth.selectedIndex = thisMonth;
inst.hMonth.onchange = () => { picker.draw(id); };
inst.hPick.appendChild(inst.hMonth);
// (A3-3) HTML YEAR SELECT
inst.hYear = document.createElement("select");
inst.hYear.classList.add("picker-y");
for (let y = thisYear-inst.yrange; y < thisYear+inst.yrange; y++) {
temp = document.createElement("option");
temp.value = y;
temp.text = y;
inst.hYear.appendChild(temp);
}
inst.hYear.selectedIndex = inst.yrange;
inst.hYear.onchange = () => { picker.draw(id); };
inst.hPick.appendChild(inst.hYear);
// (A3-4) HTML DAYS
inst.hDays = document.createElement("div");
inst.hDays.classList.add("picker-d");
inst.hPick.appendChild(inst.hDays);
picker.draw(id);
// (A4) INLINE DATEPICKER - ATTACH INTO CONTAINER
if (inst.container) { inst.container.appendChild(inst.hPick); }
// (A5) POPUP DATEPICKER - ATTACH INTO HTML BODY
else {
// (A5-1) FULL PAGE WRAPPER
inst.hWrap = document.createElement("div");
inst.hWrap.classList.add("picker-wrap");
inst.hWrap.appendChild(inst.hPick);
// (A5-2) CLICK TO TOGGLE DATEPICKER
inst.target.onfocus = () => {
inst.hWrap.classList.add("show");
};
inst.hWrap.onclick = (evt) => { if (evt.target == inst.hWrap) {
inst.hWrap.classList.remove("show");
}};
// (A5-3) ATTACH POPUP DATEPICKER
document.body.appendChild(inst.hWrap);
}
}
You have already seen this one – picker.attach()
is used to attach the date picker to your specified container. It’s long-winded but straightforward:
- Register the date picker and options into
picker.instances
. Yes, this is to distinguish which is which if multiple date pickers are created on the same page. - Generate the HTML of the date picker – We will go through that below.
PART B) DRAW DAYS IN MONTH
// (B) DRAW DAYS IN MONTH
draw : (id) => {
// (B1) CRAZY VARS & CALCULATIONS
// (B1-1) GET INSTANCE + SELECTED MONTH YEAR
let inst = picker.instances[id],
month = inst.hMonth.value,
year = inst.hYear.value;
// (B1-2) DATE RANGE CALCULATION (UTC+0)
let daysInMonth = new Date(Date.UTC(year, month, 0)).getUTCDate(),
startDay = new Date(Date.UTC(year, month-1, 1)).getUTCDay(), // SUN IS 0
endDay = new Date(Date.UTC(year, month-1, daysInMonth)).getUTCDay();
startDay = startDay==0 ? 7 : startDay,
endDay = endDay==0 ? 7 : endDay;
// (B1-3) TODAY (FOR HIGHLIGHTING "TODAY'S DATE CELL")
let today = new Date(), todayDate = null;
if (today.getUTCMonth()+1 == month && today.getUTCFullYear() == year) {
todayDate = today.getUTCDate();
}
// (B1-4) DAY NAMES
let daynames = ["Mon", "Tue", "Wed", "Thur", "Fri", "Sat"];
if (inst.startmon) { daynames.push("Sun"); }
else { daynames.unshift("Sun"); }
// (B1-5) FOR GENERATING DATE SQUARES
let table, row, cell, squares = [];
// (B2) CALCULATE DATE SQUARES ARRAY
// (B2-1) EMPTY SQUARES BEFORE FIRST DAY OF MONTH
if (inst.startmon && startDay!=1) {
for (let i=1; i<startDay; i++) { squares.push("B"); }
}
if (!inst.startmon && startDay!=7) {
for (let i=0; i<startDay; i++) { squares.push("B"); }
}
// (B2-2) DAYS OF MONTH (SOME DAYS DISABLED)
if (inst.disableday) {
let thisDay = startDay;
for (let i=1; i<=daysInMonth; i++) {
squares.push([i, inst.disableday.includes(thisDay)]);
thisDay++;
if (thisDay==8) { thisDay = 1; }
}
}
// (B2-3) DAYS OF MONTH (ALL DAYS ENABLED)
else {
for (let i=1; i<=daysInMonth; i++) { squares.push([i, false]); }
}
// (B2-4) EMPTY SQUARES AFTER LAST DAY OF MONTH
if (inst.startmon && endDay!=7) {
for (let i=endDay; i<7; i++) { squares.push("B"); }
}
if (!inst.startmon && endDay!=6) {
for (let i=endDay; i<(endDay==7?13:6); i++) { squares.push("B"); }
}
// (B3) DRAW HTML
// (B3-1) HTML DAY NAMES HEADER
table = document.createElement("table");
row = table.insertRow();
row.classList.add("picker-d-h");
for (let d of daynames) {
cell = row.insertCell();
cell.innerHTML = d;
}
// (B3-2) HTML DATE CELLS
row = table.insertRow();
for (let i=0; i<squares.length; i++) {
if (i!=squares.length && i%7==0) { row = table.insertRow(); }
cell = row.insertCell();
if (squares[i] == "B") { cell.classList.add("picker-d-b"); }
else {
// CELL DATE
cell.innerHTML = squares[i][0];
// NOT ALLOWED TO CHOOSE THIS DAY
if (squares[i][1]) { cell.classList.add("picker-d-dd"); }
// ALLOWED TO CHOOSE THIS DAY
else {
if (squares[i][0] == todayDate) { cell.classList.add("picker-d-td"); }
cell.classList.add("picker-d-d");
cell.onclick = () => { picker.pick(id, squares[i][0]); }
}
}
}
// (B4) ATTACH DAYS TO DATEPICKER
inst.hDays.innerHTML = "";
inst.hDays.appendChild(table);
}
This function deals with the generation of the HTML days in months. It is fired from picker.attach()
, and whenever the month/year is changed. Some crazy calculations are involved here:
- (B1) Get the selected month/year. Calculate the days in the month, the start date, end date, etc…
- (B2) Take note, we do not generate the HTML immediately. We calculate the days and keep them in the array
squares
.- Basically, every month is not guaranteed to start on a Sunday (or Monday). So there may be a number of “blank squares” before the first day of the month – This is padded by pushing
B
intosquares
. - Then the individual days will be populated into
squares
, these are arrays with 2 elements. The first element is the day of the month, the second is a boolean to indicate if this day is disabled. - Every month is also not guaranteed to end on a Saturday (or Sunday). So if necessary, we do the same and pad
squares
withB
after the last day of the month.
- Basically, every month is not guaranteed to start on a Sunday (or Monday). So there may be a number of “blank squares” before the first day of the month – This is padded by pushing
- (B3 & B4) The actual HTML generation.
PART C) PICK A DATE
// (C) CHOOSE A DATE
pick : (id, day) => {
// (C1) GET MONTH YEAR
let inst = picker.instances[id],
month = inst.hMonth.value,
year = inst.hYear.value;
// (C2) FORMAT & SET SELECTED DAY (YYYY-MM-DD)
if (+month<10) { month = "0" + month; }
if (+day<10) { day = "0" + day; }
inst.target.value = `${year}-${month}-${day}`;
// (C3) POPUP ONLY - CLOSE
if (inst.container === undefined) {
inst.hWrap.classList.remove("show");
}
// (C4) CALL ON PICK IF DEFINED
if (inst.onpick) { inst.onpick(); }
}
As you can guess, this function simply gets the selected date and returns a nicely formatted string. I have set it to use the ISO 8601 date format of YYYY-MM-DD, but feel free to change it as you see fit.
EXTRA) DATE PICKER HTML & CSS
This is the HTML that the Javascript generates… A little messy, but feel free to change the CSS theme to your own liking.
USEFUL BITS & LINKS
That’s all for this project, and here is a small section on some extras that may be useful to you.
DISABLING CERTAIN DAYS
Everyone has a different requirement –
- Disable specific days.
- Disable past days.
- Allow only past days (disable future days).
- Only enable from tomorrow to N days later.
- Disable X on the first and third weeks of the month, disable Y on the second and fourth weeks of the month.
Yep, there is no end to these “custom rules”. If I implement all of them, it won’t be a “simple date picker” anymore. So it’s up to you to do your own custom restrictions. Long story short –
- (A) Add your own custom restriction option to
attach()
. - (B) Modify
draw()
to also include your new restrictions.
DO SERVER-SIDE CHECKS!
Yes, the date picker is based on UTC+0, but “errors” still can happen. For you guys who are lost, the dates are based on the users’ devices. This can be easily changed, and potentially mess things up. So always do checks when the date/time is submitted, based on the server’s current time.
DATE RANGE PICKER
Sadly, this is another one that is “not simple”. Modify draw()
and pick()
if you wish. But otherwise, the easy way is to just create 2 date pickers – “Starting” and “until”. Upon submission, check that “until” is a later date than “starting”.
COMPATIBILITY CHECKS
- Arrow Functions – CanIUse
- Template Literals – CanIUse
- Viewport Units – CanIUse
- 2D CSS Transform – CanIUse
Works on all modern browsers. Not on the old Internet Exploders.
THE END
Thank you for reading, and we have come to the end of this guide. I hope that it has helped you with your project, and if you have anything to share with this guide, please feel free to comment below. Good luck and happy coding!
Hey, Idk if you care about this change, but instead of var cell = null, you added cell = null. It messed me up with es6
Good catch, missing a comma.
Downloaded the date picker and like it a lot. How can I get the date picker make the max date today’s date. And limit the months and years from a start date that is provided. Basically I have data from the past, that starts on specific date. And data is only available up to today. I’m not java developer, but I tried some things but things did not work out well.
https://code-boxx.com/faq/#help
Also see “DISABLING CERTAIN DAYS” above.
I noticed the current “today” day cell is not highlighted … so I was going to add “`cell.classList.add(‘picker-d-today’)“` …how would you go about this?
Updated – Small calculations added in
(B7-2) HTML DATE CELLS
Hi, in firefox, the class “picker-d-td” is added to the first day of the week instead of “today”. For today, it’s October 10th that is highlighted. If you try the “2-picker-option.html”, it’s October 11th that is highlighted. In fact, even on your website it’s October 10th that is highlighted.
Have you the same problem?
Fixed – Minor calculation error.
Great calendar! I changed some of the CSS, and added a disable past dates option:
line 16 I added:
lines 107-109 I added:
and lines 144-154 I added:
Thanks for sharing – But this code fragment is wrong and will not work.
if (year == thisYear && month == thisMonth && thisdate < i)
only covers the current month/year - It should also check all past months and years as well.Oh, right. Thanks for pointing that out! Because I didn’t need past dates I also changed it to only print year options from the current year forward.
Hi W.S. Toh,
Excellent work on this Datepicker, simply superb!
Could you please suggest a better way to disable past dates, if not as per frostycoles’ suggestion? I’d really appreciate your further kind help.
Also, I’m wondering how to keep the selected dates highlighted.
You’re truly a genius at Javascripting.
Looking forward to your kind reply, thanks!
(A1) Introduce your own
disablepast
option.(A3) Allow future months and years only.
(B2) Disable all dates that are before
today
.Thanks a lot for your response, W.S. Toh. However, I couldn’t figure it out. I guess I’m not an expert like you. Please can you enlighten me as well as others by elaborating on the below advise? Many thanks ☺
(A1) Introduce your own disablepast option.
(A3) Allow future months and years only.
(B2) Disable all dates that are before today.
I shall consider this as a request. Good luck with your project.
https://code-boxx.com/faq/#help
disable past days would be very usefull (and logic) 🙂 due to lack of scripting skills i don’t now how to make this happen…. anybody?
Thanks a lot, your calendar helped a lot. Please write how to do it so that you can choose a date range. For example, I click on one date in the calendar, and then click on the second, and the entire interval between them is highlighted and recorded.
Read “DATE RANGE PICKER” above.
Thanks for creating this–it was ideal for my app. I needed a lightweight API, and this fit the bill perfectly. The functionality is smooth, and the look and feel is nice, too.
One small bug I noted on the pop-up version of the code: the day is off by one. For instance, the picker shows July 1st, 2020 as falling on a Tuesday, but July 1st, 2020 falls on a Wednesday. I incremented startDay by one and that resolved the issue.
let startDay = new Date(year + “-” + month + “-1”).getDay(); // Note: Sun = 0
startDay++; // made the change here
Thanks again for creating this, W.S.! Very nice widget!
Sorry, I am unable to replicate the bug here – Both versions use the same function to calculate the dates, and they are correctly starting on Wed. I can only guess that it has something to do with the local time setting on your PC.
Hello. Just as a quick follow-up: I found that by using getUTCDay (using universal time) instead of getDay, the problem in FireFox went away.
let startDay = new Date(year + “-” + month + “-1”).getUTCDay(); // Note: Sun = 0
let endDay = new Date(year + “-” + month + “-” + daysInMonth).getUTCDay();
Got it. Long story short, the timezone is causing the bug here… Instead of doing “if Firefox, date+1”, I just fixed all calculations to be based on UTC+0 now.
I have run into a problem implementing the dark popup datepicker. The background of the datepicker is white, not dark gray. The blanks cells are the proper color. The day names and numbers are white, giving the appearance that they are empty. When you hover over a day number the background turns red and the day number is visible.
I used picker-dark-pop.css and datepicker-pop.js out of the box, without modification. I believe the background is set in the CSS under .picker.
Can you suggest where i might look for this problem?
Thanks
As above,
.picker { background: COLOR }
sets the background color of the date picker. It is probably overridden somewhere in your project, try adding!important
, or do some of your own debugging (right click > inspect element > styles).Will this datepicker work when the Content Security Policy is script=’src’?
Sorry, I don’t catch your question nor what you are trying to achieve here. Setting the Content Security Policy (CSP) is totally unnecessary unless you are trying to do cross-site (cross-domain) scripting or preventing XSS – See the tutorial on MDN.
That is an advanced topic, totally unrelated to this datepicker?
thank you man! 😀
i have a question, how can i disable the weekends and past days?