Creating A POS System With Pure HTML CSS Javascript

Welcome to a tutorial and sharing on how to create a POS system using only HTML, CSS, and Javascript. Yes, you read that right, there are no server-side scripts involved in this little experiment. If you are curious as to how viable it is to create a “standalone web app” with modern Javascript – Yes, we can, and it is very possible.

But be warned, this is not a “complete POS system”, but a “proof of concept”. There are experimental technologies (as at the time of writing) used in this project, and it is intended for the advanced code ninjas only. Brace yourselves, it is not going to be as easy, but exciting nonetheless. 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 & Notes Products Cart
Orders Useful Bits & Links The End

 

DOWNLOAD & NOTES

Firstly, here is the download link to the example code as promised.

 

EXAMPLE CODE DOWNLOAD

Click here to download all the example 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

  • While this is pure Javascript, you will still need a webserver and access with http:// for some of the features to work.

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.

 

 

PRODUCTS

All right, let us now get started with the simplest of them all – The list of products available for sale.

 

POS SYSTEM LANDING PAGE

JS-POS.html
<!-- (A) PRODUCTS LIST -->
<div id="poslist"></div>

<!-- (B) CART -->
<div id="poscart"></div>

<!-- (C) NINJA RECEIPT -->
<div id="posreceipt"></div>

Yep. Not going to go crazy on the interface.

  • <div id="poslist"> The list of products.
  • <div id="poscart"> The current “shopping cart”.
  • <div id="posreceipt"> A hidden wrapper to generate the receipt for printing.

 

PRODUCTS JAVASCRIPT

JS-POS.js
// (A) PRODUCTS LIST
list : {
  1 : { name:"Banana", img:"banana.png", price: 12 },
  2 : { name:"Cherry", img:"cherry.png", price: 23 },
  3 : { name:"Ice Cream", img:"icecream.png", price: 54 },
  4 : { name:"Orange", img:"orange.png", price: 65 },
  5 : { name:"Strawberry", img:"strawberry.png", price: 34 },
  6 : { name:"Watermelon", img:"watermelon.png", price: 67 }
}

Plain and simple – Since we do not have a server-side database, the products have to be stored somewhere else in an array or object. If it is not obvious enough, this one is in the format of PRODUCT ID : { PRODUCT DETAILS }.

P.S. If you have a lot of products, it makes more sense to save them in an indexedDB store. I am just being lazy here.

 

 

GENERATE PRODUCTS LIST

JS-POS.js
// (B) DRAW HTML PRODUCTS LIST
draw : function () {
  // (B1) TARGET WRAPPER
  var wrapper = document.getElementById("poslist");
 
  // (B2) CREATE PRODUCT HTML
  for (let pid in products.list) {
    // CURRENT PRODUCT
    let p = products.list[pid],
        pdt = document.createElement("div"),
        segment;
 
    // PRODUCT SEGMENT
    pdt.className = "pwrap";
    pdt.dataset.pid = pid;
    pdt.onclick = cart.add;
    wrapper.appendChild(pdt);

    // IMAGE
    segment = document.createElement("img");
    segment.className = "pimg";
    segment.src = "images/" + p.img;
    pdt.appendChild(segment);
 
    // NAME
    segment = document.createElement("div");
    segment.className = "pname";
    segment.innerHTML = p.name;
    pdt.appendChild(segment);
 
    // PRICE
    segment = document.createElement("div");
    segment.className = "pprice";
    segment.innerHTML = "$" + p.price;
    pdt.appendChild(segment);
  }
}
 
window.addEventListener("DOMContentLoaded", products.draw);

The products.draw() function gets called on page load. Very straightforward, it simply generates the list of products into <div id="poslist">.

 

SHOPPING CART

Next, let us deal with the “shopping cart”. This one is mostly adapted from my other shopping cart tutorial.

 

LOCAL STORAGE CART ENGINE

JS-POS.js
// (A) PROPERTIES
items : {}, // CURRENT ITEMS IN CART

// (B) SAVE CURRENT CART INTO LOCALSTORAGE
save : function () {
  localStorage.setItem("cart", JSON.stringify(cart.items));
},

// (C) LOAD CART FROM LOCALSTORAGE
load : function () {
  cart.items = localStorage.getItem("cart");
  if (cart.items == null) { cart.items = {}; }
  else { cart.items = JSON.parse(cart.items); }
},

// (D) NUKE CART!
nuke : function () {
  cart.items = {};
  localStorage.removeItem("cart");
  cart.list();
}
  • For a start, all selected products will be stored in the cart.items object in the format of PRODUCT ID : QUANTITY.
  • We don’t want the cart to “disappear” on accidental page reloads, so we save them into the localStorage instead.
  • The 3 related functions should be self-explanatory.
    • cart.save() JSON encodes cart.items and stores it into localStorage.
    • cart.load() JSON decodes localStorage and restores it into cart.items.
    • Lastly, cart.nuke() clears out the entire cart.

 

LIST CURRENT CART ITEMS

JS-POS.js
// (E) INITIALIZE - RESTORE PREVIOUS SESSION
init : function () {
  cart.load();
  cart.list();
},
 
// (F) LIST CURRENT CART ITEMS (IN HTML)
list : function () {
  // (F1) DRAW CART INIT
  var wrapper = document.getElementById("poscart"),
      item, part, pdt,
      total = 0, subtotal = 0,
      empty = true;
  wrapper.innerHTML = "";
  for (let key in cart.items) {
    if (cart.items.hasOwnProperty(key)) { empty = false; break; }
  }
 
  // (F2) CART IS EMPTY
  if (empty) {
    item = document.createElement("div");
    item.innerHTML = "Cart is empty";
    wrapper.appendChild(item);
  }
 
  // (F3) CART IS NOT EMPTY - LIST ITEMS
  else {
    for (let pid in cart.items) {
      // CURRENT ITEM
      pdt = products.list[pid];
      item = document.createElement("div");
      item.className = "citem";
      wrapper.appendChild(item);
 
      // ITEM NAME
      part = document.createElement("span");
      part.innerHTML = pdt.name;
      part.className = "cname";
      item.appendChild(part);
 
      // REMOVE
      part = document.createElement("input");
      part.type = "button";
      part.value = "X";
      part.dataset.pid = pid;
      part.className = "cdel";
      part.addEventListener("click", cart.remove);
      item.appendChild(part);
 
      // QUANTITY
      part = document.createElement("input");
      part.type = "number";
      part.min = 0;
      part.value = cart.items[pid];
      part.dataset.id = pid;
      part.className = "cqty";
      part.addEventListener("change", cart.change);
      item.appendChild(part);
 
      // SUBTOTAL
      subtotal = cart.items[pid] * pdt.price;
      total += subtotal;
    }
 
    // TOTAL AMOUNT
    item = document.createElement("div");
    item.className = "ctotal";
    item.id = "ctotal";
    item.innerHTML ="TOTAL: $" + total;
    wrapper.appendChild(item);
 
    // EMPTY BUTTON
    item = document.createElement("input");
    item.type = "button";
    item.value = "Empty";
    item.addEventListener("click", cart.nuke);
    item.id = "cempty";
    wrapper.appendChild(item);
 
    // CHECKOUT BUTTON
    item = document.createElement("input");
    item.type = "button";
    item.value = "Checkout";
    item.addEventListener("click", cart.checkout);
    item.id = "ccheckout";
    wrapper.appendChild(item);
  }
}
window.addEventListener("DOMContentLoaded", cart.init);

Right, keep calm and look carefully. This piece of long stinky function is actually plain stupid simple:

  • cart.init() gets called on page load to restore the previous session, draw the list of cart items.
  • cart.list() just a whole bunch of code to generate the current cart items into <div id="poscart">.

 

 

SHOPPING CART ACTIONS

JS-POS.js
// (G) ADD ITEM TO CART
add : function () {
  var pid = this.dataset.pid;
  if (cart.items[pid] == undefined) { cart.items[pid] = 1; }
  else { cart.items[pid]++; }
  cart.save(); cart.list();
},
 
// (H) CHANGE QUANTITY
change : function () {
  // (H1) REMOVE ITEM
  var pid = this.dataset.pid;
  if (this.value <= 0) {
    delete cart.items[pid];
    cart.save(); cart.list();
  }

  // (H2) UPDATE TOTAL ONLY
  else {
    cart.items[pid] = this.value;
    var total = 0;
    for (let id in cart.items) {
      total += cart.items[pid] * products.list[pid].price;
      document.getElementById("ctotal").innerHTML ="TOTAL: $" + total;
    }
  }
},
 
// (I) REMOVE ITEM FROM CART
remove : function () {
  delete cart.items[this.dataset.pid];
  cart.save(); cart.list();
},
 
// (J) CHECKOUT
checkout : function () {
  orders.print();
  orders.add();
}

Lastly, we have the “common cart actions”. Self-explanatory once again:

  • cart.add() Adds the chosen item into the cart.
  • cart.change() Change item quantity.
  • cart.remove() Remove item from the cart.
  • cart.checkout() Check out the current cart, print the receipt, and save the order into indexedDB.

 

 

ORDERS DATABASE

Finally, we save the order into the local indexedDB on “checkout”.

 

DATABASE INITIALIZATION

JS-POS.js
// (A) PROPERTY
idb : window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB,
posdb : null,
db : null,
 
 // (A) INIT - CREATE DATABASE
init : function () {
  // (A1) INDEXED DATABASE OBJECT
  if (!orders.idb) {
    alert("INDEXED DB IS NOT SUPPORTED ON THIS BROWSER!");
    return false;
  }

  // (A2) OPEN POS DATABASE
  orders.posdb = orders.idb.open("JSPOS", 1);
  orders.posdb.onsuccess = function () {
    orders.db = orders.posdb.result;
  };
 
  // (A3) CREATE POS DATABASE
  orders.posdb.onupgradeneeded = function () {
    // ORDERS STORE (TABLE)
    var db = orders.posdb.result,
        store = db.createObjectStore("Orders", {keyPath: "oid", autoIncrement: true}),
        index = store.createIndex("time", "time");
 
    // ORDER ITEMS STORE (TABLE)
    store = db.createObjectStore("Items", {keyPath: ["oid", "pid"]}),
    index = store.createIndex("qty", "qty");
  };
 
  // (A4) ERROR!
  orders.posdb.onerror = function (err) {
    alert("ERROR CREATING DATABASE!");
    console.log(err);
  };
}
window.addEventListener("DOMContentLoaded", orders.init);

Yes, there is something called “IndexedDB”, and it is supported in nearly all modern browsers. The difference between indexedDB and localStorage is that:

  • indexedDB is a “lightweight database”, while localStorage is meant to store “single variables”.
  • indexedDB does not have a storage size limit (technically), while localStorage has a very small sandbox limit.

So it is the perfect candidate to store all the orders. If you want to learn more about it, I shall leave some links below. But for now, this initialization will create 2 simple stores (or tables):

  • Orders The orders. Doh.
    • oid Order ID. Primary key and auto-increment.
    • time Time when the order is made.
  • Items Order items.
    • oid Order ID. Composite primary key.
    • pid Product ID. Composite primary key.
    • qty Quantity ordered.

I have kept this as simple as possible, feel free to add your own touch.

 

ADD NEW ORDER (ON CART CHECKOUT)

JS-POS.js
 // (B) ADD NEW ORDER
add : function () {
  // (B1) INSERT ORDERS STORE (TABLE)
  var tx = orders.db.transaction("Orders", "readwrite"),
      store = tx.objectStore("Orders"),
      req = store.put({time: Date.now()});
 
  // (B2) THE PAINFUL PART - INDEXED DB IS ASYNC
  // HAVE TO WAIT UNTIL ALL IS ADDED TO DB BEFORE CLEAR CART
  // THIS IS TO TRACK THE NUMBER OF ITEMS ADDED TO DATABASE
  var size = 0, entry;
  for (entry in cart.items) {
    if (cart.items.hasOwnProperty(entry)) { size++; }
  }
 
  // (B3) INSERT ITEMS STORE (TABLE)
  entry = 0;
  req.onsuccess = function (e) {
    tx = orders.db.transaction("Items", "readwrite"),
    store = tx.objectStore("Items"),
    oid = req.result;
    for (let pid in cart.items) {
      req = store.put({oid: oid, pid: pid, qty: cart.items[pid]});

      // (B4) EMPTY CART ONLY AFTER ALL ENTRIES SAVED
      req.onsuccess = function () {
        entry++;
        if (entry == size) { cart.nuke(); }
      };
    }
  };
}

Not going to explain line-by-line, but this is the equivalent of:

  • INSERT INTO `orders`
  • Loop through the cart – INSERT INTO `items` (OID, PID, QTY)

Take extra note of how the cart is emptied only after everything is saved… Yep, the transactions are asynchronous, and you have to be extra careful with it. Also, for you guys who are not sure where to check the database, it is right inside the developer’s console. For Chrome users, Application > IndexedDB.

 

 

PRINT THE RECEIPT

JS-POS.js
 // (C) PRINT RECEIPT FOR CURRENT ORDER
print : function () {
  // (C1) GENERATE RECEIPT
  var wrapper = document.getElementById("posreceipt");
  wrapper.innerHTML = "";
  for (let pid in cart.items) {
    let item = document.createElement("div");
    item.innerHTML = `${cart.items[pid]} X ${products.list[pid].name}`;
    wrapper.appendChild(item);
  }
 
  // (C2) PRINT
  var printwin = window.open();
  printwin.document.write(wrapper.innerHTML);
  printwin.stop();
  printwin.print();
  printwin.close();
}

Just generating the HTML receipt and printing it out.

 

USEFUL BITS & LINKS

That’s all for the tutorial, and here is a small section on some extras and links that may be useful to you.

 

PERSISTENT DATA STORAGE

A small word of warning here – The IndexedDB can still be deleted by a “clear cache”. It is best to look into persistent storage and ways to export/backup into an external file.

 

BARCODE SCANNING

  • We can easily attach a USB barcode scanner these days… Should not be much of an issue, just create your own <input type="text"/> barcode scan field.
  • Alternatively, Javascript can also access the camera to scan codes – QuaggaJS

 

PAYMENT PROCESSING

This is the rather difficult part. Cash payments are not an issue, but card payments are. To process card payments, you will most likely still need a server-side script and Internet access to process it. Also, check out the Payment Request API if you are interested.

 

PRINTING PAINS

Lastly, we can easily connect a thermal printer to the POS device. But the problem with web-based is that we cannot print directly, a “select printer” popup will show every time due to security restrictions… Unless you have some funky browser plugins to bypass that.

 

MORE LINKS & REFERENCES

 

THE END

Thank you for reading, and we have come to the end. I hope that it has helped you to better understand, and if you want to share anything with this guide, please feel free to comment below. Good luck and happy coding!

2 thoughts on “Creating A POS System With Pure HTML CSS Javascript”

  1. hi,
    I am grateful for your unselfish sharing of knowledge.
    I am just new wishing to learn html and javascript, your tutorial is great and clear.

    By the way, i would like to know your email and i want to contact you privately because I need to buy from you a program. I would rather buy from you than anywhere else.
    I hope to see your email reply soonest.

    Thank you,

    roger

Leave a Comment

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