POS System With Pure HTML CSS JS (Free Download)

Welcome to a tutorial on how to create a POS system with pure 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 of 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!

 

 

TABLE OF CONTENTS

 

PART 1) 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-products.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-products.js
// (B) DRAW HTML PRODUCTS LIST
draw : () => {
  // (B1) TARGET WRAPPER
  const 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.onclick = () => cart.add(pid);
    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">.

 

PART 2) 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-cart.js
// (A) PROPERTIES
items : {}, // current items in cart

// (B) SAVE CURRENT CART INTO LOCALSTORAGE
save : () => localStorage.setItem("cart", JSON.stringify(cart.items)),

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

// (D) NUKE CART!
nuke : () => {
  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-cart.js
// (E) INITIALIZE - RESTORE PREVIOUS SESSION
init : () => {
  cart.load();
  cart.list();
},
 
// (F) LIST CURRENT CART ITEMS (IN HTML)
list : () => {
  // (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.className = "cdel";
      part.onclick = () => cart.remove(pid);
      item.appendChild(part);
 
      // QUANTITY
      part = document.createElement("input");
      part.type = "number";
      part.min = 0;
      part.value = cart.items[pid];
      part.className = "cqty";
      part.onchange = function () { cart.change(pid, this.value); };
      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.onclick = cart.nuke;
    item.id = "cempty";
    wrapper.appendChild(item);
 
    // CHECKOUT BUTTON
    item = document.createElement("input");
    item.type = "button";
    item.value = "Checkout";
    item.onclick = 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-cart.js
// (G) ADD ITEM TO CART
add : pid => {
  if (cart.items[pid] == undefined) { cart.items[pid] = 1; }
  else { cart.items[pid]++; }
  cart.save(); cart.list();
},
 
// (H) CHANGE QUANTITY
change : (pid, qty) => {
  // (H1) REMOVE ITEM
  if (qty <= 0) {
    delete cart.items[pid];
    cart.save(); cart.list();
  }

  // (H2) UPDATE TOTAL ONLY
  else {
    cart.items[pid] = qty;
    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 : pid => {
  delete cart.items[pid];
  cart.save(); cart.list();
},
 
// (J) CHECKOUT
checkout : () => {
  orders.print();
  orders.add();
}

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

  • cart.add() Adds the chosen item to 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.

 

 

PART 3) ORDERS DATABASE

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

 

DATABASE INITIALIZATION

JS-POS-orders.js
// (A) PROPERTIES
iName : "JSPOS",
iDB : null, iOrders : null, iItems : null,
 
// (B) INIT
init : () => {
  // (B1) REQUIREMENTS CHECK - INDEXED DB
  window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
  if (!window.indexedDB) {
    alert("Your browser does not support indexed database.");
    return;
  }
 
  // (B2) OPEN IDB
  let req = window.indexedDB.open(orders.iName, 1);
 
  // (B3) IDB OPEN ERROR
  req.onerror = evt => {
    alert("Indexed DB init error - " + evt.message);
    console.error(evt);
  };
 
  // (B4) IDB UPGRADE NEEDED
  req.onupgradeneeded = evt => {
    orders.iDB = evt.target.result;
 
    // (B4-1) IDB UPGRADE ERROR
    orders.iDB.onerror = evt => {
      alert("Indexed DB upgrade error - " + evt.message);
      console.error(evt);
    };
 
    // (B4-2) IDB VERSION 1
    if (evt.oldVersion < 1) {
      // ORDERS STORE
      let store = orders.iDB.createObjectStore("Orders", {
        keyPath: "oid",
        autoIncrement: true
      });
      store.createIndex("time", "time");
 
      // ORDER ITEMS STORE
      store = orders.iDB.createObjectStore("Items", {
        keyPath: ["oid", "pid"]
      });
      store.createIndex("qty", "qty");
    }
  }
}
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.
    • oid Order ID. Key and auto-increment.
    • time Time when the order is made.
  • Items Order items.
    • oid Order ID. Composite key.
    • pid Product ID. Composite 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-orders.js
// (C) ADD NEW ORDER
add : () => {
  // (C1) INSERT ORDERS STORE
  let req = orders.iOrders().put({ time: Date.now() });
 
  // (C2) 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++; }
  }
 
  // (C3) INSERT ITEMS STORE
  entry = 0;
  req.onsuccess = e => {
    oid = req.result;
    for (let pid in cart.items) {
      req = orders.iItems().put({ oid: oid, pid: pid, qty: cart.items[pid] });
      req.onsuccess = () => {
        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-orders.js
// (D) PRINT RECEIPT FOR CURRENT ORDER
print : () => {
  // (D1) 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);
  }
 
  // (D2) 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.

 

STEP 4) PROGRESSIVE WEB APP

This is optional, but to further enhance the Javascript POS – Let us turn it into a PWA. It’s not that difficult. Really.

 

MANIFEST

JS-POS-manifest.json
{
  "short_name": "JS POS",
  "name": "JS POS",
  "icons": [{
    "src": "images/favicon.png",
    "sizes": "64x64",
    "type": "image/png"
  }, {
    "src": "images/icon-512.png",
    "sizes": "512x512",
    "type": "image/png"
  }],
  "start_url": "JS-POS.html",
  "scope": "/",
  "background_color": "white",
  "theme_color": "white",
  "display": "standalone"
}

To “register” the POS as a “web app”, we need to create a manifest file. This should be self-explanatory, we are just specifying the app name, icons, and some settings.

 

SERVICE WORKER

JS-POS-worker.js
// (A) CREATE/INSTALL CACHE
self.addEventListener("install", evt => {
  self.skipWaiting();
  evt.waitUntil(
    caches.open("JSPOS")
    .then(cache => cache.addAll([
      "JS-POS.html",
      "JS-POS.css",
      "JS-POS.js",
      "images/banana.png",
      "images/cherry.png",
      "images/favicon.png",
      "images/icecream.png",
      "images/icon-512.png",
      "images/orange.png",
      "images/strawberry.png",
      "images/watermelon.png"
    ]))
    .catch(err => console.error(err))
  );
});
 
// (B) CLAIM CONTROL INSTANTLY
self.addEventListener("activate", evt => self.clients.claim());
 
// (C) LOAD FROM CACHE FIRST, FALLBACK TO NETWORK IF NOT FOUND
self.addEventListener("fetch", evt => evt.respondWith(
  caches.match(evt.request).then(res => res || fetch(evt.request))
));

The manifest file itself is not sufficient to turn the POS into a “PWA”. Here, we have a simple service worker that caches all the HTML, CSS, JS, and image files – Yes, this literally turns the POS into an offline-capable app.

 

HTML META

JS-POS.html
<!-- MANIFEST + SERVICE WORKER -->
<link rel="manifest" href="JS-POS-manifest.json">
<script>
if ("serviceWorker" in navigator) {
  navigator.serviceWorker.register("JS-POS-worker.js");
}
</script>

Finally, we specify the worker and manifest file in the <head> section. With that, the Javascript POS is now a full-fledged installable PWA.

 

DOWNLOAD & NOTES

Here is the download link to the example code, so you don’t have to copy-paste everything.

 

SORRY FOR THE ADS...

But someone has to pay the bills, and sponsors are paying for it. I insist on not turning Code Boxx into a "paid scripts" business, and I don't "block people with Adblock". Every little bit of support helps.

Buy Me A Coffee Code Boxx eBooks

 

EXAMPLE CODE DOWNLOAD

Click here for the source code on GitHub gist, just click on “download zip” or do a git clone. I have released it under the MIT license, so feel free to build on top of it or use it in your own project.

 

EXTRA 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… Although we may be able to overcome this by sending a fetch request to a shared print server.

 

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 “POS System With Pure HTML CSS JS (Free Download)”

  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 *