Simple Date Picker in Pure Javascript CSS – Free Code Download

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 How It Works Useful Bits
The End

 

 

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.

 

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.

 

QUICK NOTES

If you spot a bug, please feel free to comment below. I try to answer 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.

 

QUICKSTART – BASIC USAGE

1-simple-picker.html
<!-- (A) LOAD DATE PICKER -->
<link href="dp-dark.css" rel="stylesheet">
<!-- <link href="dp-light.css" rel="stylesheet"> -->
<script src="datepicker.js"></script>
 
<!-- (B) THE HTML -->
<!-- (B1) INLINE DATE PICKER -->
<input type="text" id="input-inline" placeholder="Inline"/>
<div id="pick-inline"></div>
 
<!-- (B2) POPUP DATE PICKER -->
<input type="text" id="input-pop" placeholder="Popup"/>
 
<!-- (C) ATTACH DATE PICKER ON LOAD -->
<script>
window.addEventListener("load", function(){
  // (C1) INLINE DATE PICKER
  picker.attach({
    target: "input-inline",
    container: "pick-inline"
  });
 
  // (C2) POPUP DATE PICKER
  picker.attach({
    target: "input-pop"
  });
});
</script>

The date picker can be used in 3 simple steps:

  • A – Load the date picker CSS and Javascript.
  • B – Define the HTML fields.
    • B1 – For an inline date picker, create an <input type="text"/> and <div> container.
    • B2 – For a popup date picker, just create an <input type="text"/> field.
  • C – Use picker.attach() to attach the date picker, the required options are:
    • target ID of the text field.
    • container ID of the container to generate the date picker. Omit for the popup date picker.

Inline Date Picker

Popup Date Picker

 

 

OTHER DATE PICKER OPTIONS

2-picker-options.html
<!-- (A) LOAD THE LIBRARIES -->
<link href="dp-dark.css" rel="stylesheet">
<!-- <link href="dp-light.css" rel="stylesheet"> -->
<script src="datepicker.js"></script>
 
<!-- (B) THE HTML -->
<h1>Date Picker Options</h1>
<input type="text" id="input-opt"/>
<div id="pick-opt"></div>
 
<!-- (C) ATTACH DATE PICKER ON LOAD -->
<script>
window.addEventListener("load", function(){
  picker.attach({
    target: "input-opt",
    container: "pick-opt",
    disableday : [2, 7], // DISABLE TUE, SUN
    startmon: true // WEEK START ON MON
  });
});
</script>

Yep, there are also a few other options for the date picker:

  • 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 instead. By default, this is false and will start the week on Sunday.

Date Picker Options

 

 

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.

 

THE CSS

dp-dark.css
/* (A) POPUP */
.picker-wrap {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  background: rgba(0,0,0,0.5);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.2s;
}
.picker-wrap.show {
  opacity: 1;
  visibility: visible;
}
.picker-wrap .picker { 
  margin: 50vh auto 0 auto;
  transform: translateY(-50%);
}

/* (B) CONTAINER */
.picker {
  max-width: 300px;
  background: #444444;
  padding: 10px;
}

/* (C) MONTH + YEAR */
.picker-m, .picker-y {
  width: 50%;
  padding: 5px;
  box-sizing: border-box;
  font-size: 16px;
}

/* (D) DAY */
.picker-d table {
  color: #fff;
  border-collapse: separate;
  width: 100%;
  margin-top: 10px;
}
.picker-d table td {
  width: 14.28%; /* 7 EQUAL COLUMNS */
  padding: 5px;
  text-align: center;
}
/* HEADER CELLS */
.picker-d-h td {
  font-weight: bold;
}
/* BLANK DATES */
.picker-d-b {
  background: #4e4e4e;
}
/* TODAY */
.picker-d-td {
  background: #d84f4f;
}
/* PICKABLE DATES */
.picker-d-d:hover {
  cursor: pointer;
  background: #a33c3c;
}
/* UNPICKABLE DATES */
.picker-d-dd {
  color: #888;
  background: #4e4e4e;
}

Well, this should be pretty self-explanatory. No hidden tricks nor surprises – Feel free to change the theme to your own liking.

 

 

THE JAVASCRIPT

datepicker.js
var picker = {
  // (A) ATTACH DATEPICKER TO TARGET
  // target : datepicker will populate this field
  // container : datepicker will be generated in this container
  // startmon : start on Monday (default false)
  // disableday : array of days to disable, e.g. [2,7] to disable Tue and Sun
  attach : function (opt) {
    // (A1) CREATE NEW DATEPICKER
    var dp = document.createElement("div");
    dp.dataset.target = opt.target;
    dp.dataset.startmon = opt.startmon ? "1" : "0";
    dp.classList.add("picker");
    if (opt.disableday) {
      dp.dataset.disableday = JSON.stringify(opt.disableday);
    }

    // (A2) DEFAULT TO CURRENT MONTH + YEAR - NOTE: UTC+0!
    var today = new Date(),
        thisMonth = today.getUTCMonth(), // Note: Jan is 0
        thisYear = today.getUTCFullYear(),
        months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
                  "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];

    // (A3) MONTH SELECT
    var select = document.createElement("select"),
        option = null;
    select.classList.add("picker-m");
    for (var mth in months) {
      option = document.createElement("option");
      option.value = parseInt(mth) + 1;
      option.text = months[mth];
      select.appendChild(option);
    }
    select.selectedIndex = thisMonth;
    select.addEventListener("change", function(){ picker.draw(this); });
    dp.appendChild(select);

    // (A4) YEAR SELECT
    var yRange = 10; // Year range to show, I.E. from thisYear-yRange to thisYear+yRange
    select = document.createElement("select");
    select.classList.add("picker-y");
    for (var y = thisYear-yRange; y < thisYear+yRange; y++) {
      option = document.createElement("option");
      option.value = y;
      option.text = y;
      select.appendChild(option);
    }
    select.selectedIndex = yRange;
    select.addEventListener("change", function(){ picker.draw(this); });
    dp.appendChild(select);

    // (A5) DAY SELECT
    var days = document.createElement("div");
    days.classList.add("picker-d");
    dp.appendChild(days);

    // (A6) ATTACH DATE PICKER TO TARGET CONTAINER + DRAW THE DATES
    picker.draw(select);

    // (A6-I) INLINE DATE PICKER
    if (opt.container) { document.getElementById(opt.container).appendChild(dp); }

    // (A6-P) POPUP DATE PICKER
    else {
      // (A6-P-1) MARK THIS AS A "POPUP"
      var uniqueID = 0;
      while (document.getElementById("picker-" + uniqueID) != null) {
        uniqueID = Math.floor(Math.random() * (100 - 2)) + 1;
      }
      dp.dataset.popup = "1";
      dp.dataset.dpid = uniqueID;

      // (A6-P-2) CREATE WRAPPER
      var wrapper = document.createElement("div");
      wrapper.id = "picker-" + uniqueID;
      wrapper.classList.add("picker-wrap");
      wrapper.appendChild(dp);

      // (A6-P-3) ATTACH ONCLICK TO SHOW/HIDE DATEPICKER
      var target = document.getElementById(opt.target);
      target.dataset.dp = uniqueID;
      target.readOnly = true; // Prevent onscreen keyboar on mobile devices
      target.onfocus = function () {
        document.getElementById("picker-" + this.dataset.dp).classList.add("show");
      };
      wrapper.addEventListener("click", function (evt) {
        if (evt.target.classList.contains("picker-wrap")) {
          this.classList.remove("show");
        }
      });

      // (A6-P-4) ATTACH POPUP DATEPICKER TO DOCUMENT
      document.body.appendChild(wrapper);
    }
  },


  // (B) DRAW THE DAYS IN MONTH
  // el : HTML reference to either year or month selector
  draw : function (el) {
    // (B1) GET DATE PICKER COMPONENTS
    var parent = el.parentElement,
        year = parent.getElementsByClassName("picker-y")[0].value,
        month = parent.getElementsByClassName("picker-m")[0].value,
        days = parent.getElementsByClassName("picker-d")[0];

    // (B2) DATE RANGE CALCULATION - NOTE: UTC+0!
    var daysInMonth = new Date(Date.UTC(year, month, 0)).getUTCDate(),
        startDay = new Date(Date.UTC(year, month-1, 1)).getUTCDay(), // Note: Sun = 0
        endDay = new Date(Date.UTC(year, month-1, daysInMonth)).getUTCDay(),
        startDay = startDay==0 ? 7 : startDay,
        endDay = endDay==0 ? 7 : endDay;

    // (B3) GENERATE DATE SQUARES (IN ARRAY FIRST)
    var squares = [],
        disableday = null;
    if (parent.dataset.disableday) {
      disableday = JSON.parse(parent.dataset.disableday);
    }

    // (B4) EMPTY SQUARES BEFORE FIRST DAY OF MONTH
    if (parent.dataset.startmon=="1" && startDay!=1) {
      for (var i=1; i<startDay; i++) { squares.push("B"); }
    }
    if (parent.dataset.startmon=="0" && startDay!=7) {
      for (var i=0; i<startDay; i++) { squares.push("B"); }
    }

    // (B5) DAYS OF MONTH
    // (B5-1) ALL DAYS ENABLED, JUST ADD
    if (disableday==null) {
      for (var i=1; i<=daysInMonth; i++) { squares.push([i, false]);  }
    }

    // (B5-2) SOME DAYS DISABLED
    else {
      var thisday = startDay;
      for (var i=1; i<=daysInMonth; i++) {
        // CHECK IF DAY IS DISABLED
        var disabled = disableday.includes(thisday);
        // DAY OF MONTH, DISABLED
        squares.push([i, disabled]); 
        // NEXT DAY
        thisday++;
        if (thisday==8) { thisday = 1; }
      }
    }

    // (B6) EMPTY SQUARES AFTER LAST DAY OF MONTH
    if (parent.dataset.startmon=="1" && endDay!=7) {
      for (var i=endDay; i<7; i++) { squares.push("B"); }
    }
    if (parent.dataset.startmon=="0" && endDay!=6) {
      for (var i=endDay; i<(endDay==7?13:6); i++) { squares.push("B"); }
    }

    // (B7) DRAW HTML
    var daynames = ["Mon", "Tue", "Wed", "Thur", "Fri", "Sat"];
    if (parent.dataset.startmon=="1") { daynames.push("Sun"); }
    else { daynames.unshift("Sun"); }

    // (B7-1) HTML DATE HEADER
    var table = document.createElement("table"),
        row = table.insertRow()
        cell = null;
    row.classList.add("picker-d-h");
    for (let d of daynames) {
      cell = row.insertCell();
      cell.innerHTML = d;
    }

    // (B7-2) HTML DATE CELLS
    var total = squares.length,
        row = table.insertRow(),
        today = new Date(),
        todayDate = null;
    if (today.getUTCMonth()+1 == month && today.getUTCFullYear() == year) {
      todayDate = today.getUTCDate();
    }
    for (var i=0; i<total; i++) {
      if (i!=total && i%7==0) { row = table.insertRow(); }
      cell = row.insertCell();
      if (squares[i] == "B") { 
        cell.classList.add("picker-d-b"); 
      } else { 
        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 (i == todayDate) { cell.classList.add("picker-d-td"); }
          cell.classList.add("picker-d-d");
          cell.addEventListener("click", function(){ picker.pick(this); });
        }
      }
    }

    // (B7-3) ATTACH NEW CALENDAR TO DATEPICKER
    days.innerHTML = "";
    days.appendChild(table);
  },

  // (C) CHOOSE A DATE
  // el : HTML reference to selected date cell
  pick : function (el) {
    // (C1) GET ALL COMPONENTS
    var parent = el.parentElement;
    while (!parent.classList.contains("picker")) {
      parent = parent.parentElement;
    }

    // (C2) GET FULL SELECTED YEAR MONTH DAY
    var year = parent.getElementsByClassName("picker-y")[0].value,
        month = parent.getElementsByClassName("picker-m")[0].value,
        day = el.innerHTML;

    // YYYY-MM-DD FORMAT - CHANGE FORMAT HERE IF YOU WANT !
    if (parseInt(month)<10) { month = "0" + month; }
    if (parseInt(day)<10) { day = "0" + day; }
    var fullDate = year + "-" + month + "-" + day;

    // (C3) UPDATE SELECTED DATE
    document.getElementById(parent.dataset.target).value = fullDate;

    // (C4) POPUP ONLY - CLOSE THE POPUP
    if (parent.dataset.popup == "1") {
      document.getElementById("picker-" + parent.dataset.dpid).classList.remove("show");
    }
  }
};

Yikes! This sure looks like a lot of code, but actually, there are only 3 functions. Not going to go through them line-by-line, but here is a summary.

 

A) ATTACH

This is the function that you call to attach the date picker to your specified container. What it does is essentially, create the HTML of the date picker.

  • Create the “shell” <div class="picker">, and attach all the necessary options as data – Start on Monday, disabled days, etc…
  • The current month/year period will default to the current date on the client’s PC.
  • The rest is pretty much just generating the HTML for the month and year select.
  • Take note that the selectable days will not be generated in this function – It will be done in draw() instead.

 

 

B) DRAW

This one is fired up after attach, and whenever the month/year is changed. It generates the days for the selected period. This is kind of complicated, but actually just some calculations.

  • It starts with getting the selected month and year from the <select> boxes.
  • Then some calculations on the number of days in the month, the start/end day of the month.
  • The days will not be immediately generated into an HTML table but calculations will be kept in the array var squares first.
    • 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 into squares.
    • Then the individual days will be populated into squares – Take note that each day is an array with 2 elements. The first element is the day of the month itself. The second element is a boolean to indicate if this day is disabled.
    • The same with the last day of the month – Every month is not guaranteed to end on a Saturday (or Sunday), so there may be some “blank squares” after the last day of the month.
  • Finally, we just use the squares array to build the table.

 

C) PICK

As you can guess, this function simply gets the selected date and returns you 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.

 

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

Well, everyone probably has a different requirement –

  • Disable specific days.
  • Disable past 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 –

  • Implement your own custom option in attach(), append it to dp.dataset.NEW-RESTRICTION = opt.NEW-RESTRICTION.
  • Modify draw(), section (C) 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’ device. 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.

 

 

CHANGING THE DATE FORMAT

As above, edit pick(), section (B)var fullDate = ....

 

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”.

 

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!


17 thoughts on “Simple Date Picker in Pure Javascript CSS – Free Code Download”

  1. 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?

  2. Great calendar! I changed some of the CSS, and added a disable past dates option:
    line 16 I added:

    dp.dataset.disablepast = opt.disablepast || false;

    lines 107-109 I added:

    thisdate = new Date().getDate(),
    thisMonth = new Date().getUTCMonth() + 1,
    thisYear = new Date().getUTCFullYear();

    and lines 144-154 I added:

    var disabled = disableday.includes(thisday);
    if(JSON.parse(parent.dataset.disablepast) && !disabled){
      if(year == thisYear && month == thisMonth && thisdate < i) { disabled = true; }
    }
    1. 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.

      1. 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.

  3. 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?

  4. 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.

  5. 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!

    1. 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.

    2. 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();

      1. 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.

  6. 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

    1. 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).

    1. 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?

Leave a Comment

Your email address will not be published. Required fields are marked *