Browser tab badge notification
Asked Answered
W

3

12

I have been searching how to get a notification badge on the tab of the website page like in the image below, but couldn't find much, or anything that works. I have made a chat site and want people to know if there is a new notification in the channel they are in. But I just need to know how I can get a red badge button on the site favicon or something like that.

Preferably something like how Discord has done:

enter image description here

I have tried this, but doesn't seem to work:

var count = 0;

var title = document.title;

function changeTitle() {
    count++;
    var newTitle = '(' + count + ') ' + title;
    document.title = newTitle;
}

function newUpdate() {
    update = setInterval(changeTitle, 2000);
}
var docBody = document.getElementById('site-body');
docBody.onload = newUpdate;
Whereas answered 14/1, 2021 at 12:56 Comment(4)
They're likely updating the favicon. It's actually not that difficult to do, and there are some JavaScript libraries out there such as favico.js that will help you with this.Autoradiograph
@Lewis Is right. discord is actually updating the favicon. See these Image 1, Image 2. You can see the favicon changing in the discord head tag when you read an unread notification.Colincolinson
so they have the icons from 1 all the way up to 99?Whereas
Maybe, if done for efficiency - alternatively, it could be part of a script that generates a favicon png dynamically.Autoradiograph
Y
31

Tl;Dr: Update the <link> element with a new icon image.

enter image description here

Basically what it does:

  • takes the source of the image from the <link rel="shortcut icon" href="favicon.ico" type="image/x-icon" /> element
  • creates an in-memory Canvas element
  • applies the favicon image
  • draws the shape on top
  • draws the text value on top
  • updates the <link> element href attribute from the final composite of the canvas element imageData

Use like:

const myBadgerOptions = {}; // See: constructor for customization options
const myBadger = new Badger(myBadgerOptions);

// Live update value example:
myBadger.value = 3;

// Remove badge example:
// myBadger.value = 0;

// If needed, get the generated base64 image data: 
// console.log(myBadger.dataURL);

Badger constructor:

/**
 * Add notification badge (pill) to favicon in browser tab
 * @url stackoverflow.com/questions/65719387/
 */
class Badger {
  constructor(options) {
    Object.assign(
      this, {
        backgroundColor: "#f00",
        color: "#fff",
        size: 0.6,      // 0..1 (Scale in respect to the favicon image size)
        position: "ne", // Position inside favicon "n", "e", "s", "w", "ne", "nw", "se", "sw"
        radius: 8,      // Border radius
        src: "",        // Favicon source (dafaults to the <link> icon href)
        onChange() {},
      },
      options
    );
    this.canvas = document.createElement("canvas");
    this.src = this.src || this.faviconEL.getAttribute("href");
    this.ctx = this.canvas.getContext("2d");
  }

  faviconEL = document.querySelector("link[rel$=icon]");

  _drawIcon() {
    this.ctx.clearRect(0, 0, this.faviconSize, this.faviconSize);
    this.ctx.drawImage(this.img, 0, 0, this.faviconSize, this.faviconSize);
  }

  _drawShape() {
    const r = this.radius;
    const xa = this.offset.x;
    const ya = this.offset.y;
    const xb = this.offset.x + this.badgeSize;
    const yb = this.offset.y + this.badgeSize;
    this.ctx.beginPath();
    this.ctx.moveTo(xb - r, ya);
    this.ctx.quadraticCurveTo(xb, ya, xb, ya + r);
    this.ctx.lineTo(xb, yb - r);
    this.ctx.quadraticCurveTo(xb, yb, xb - r, yb);
    this.ctx.lineTo(xa + r, yb);
    this.ctx.quadraticCurveTo(xa, yb, xa, yb - r);
    this.ctx.lineTo(xa, ya + r);
    this.ctx.quadraticCurveTo(xa, ya, xa + r, ya);
    this.ctx.fillStyle = this.backgroundColor;
    this.ctx.fill();
    this.ctx.closePath();
  }

  _drawVal() {
    const margin = (this.badgeSize * 0.18) / 2;
    this.ctx.beginPath();
    this.ctx.textBaseline = "middle";
    this.ctx.textAlign = "center";
    this.ctx.font = `bold ${this.badgeSize * 0.82}px Arial`;
    this.ctx.fillStyle = this.color;
    this.ctx.fillText(this.value, this.badgeSize / 2 + this.offset.x, this.badgeSize / 2 + this.offset.y + margin);
    this.ctx.closePath();
  }

  _drawFavicon() {
    this.faviconEL.setAttribute("href", this.dataURL);
  }

  _draw() {
    this._drawIcon();
    if (this.value) this._drawShape();
    if (this.value) this._drawVal();
    this._drawFavicon();
  }

  _setup() {
    this.faviconSize = this.img.naturalWidth;
    this.badgeSize = this.faviconSize * this.size;
    this.canvas.width = this.faviconSize;
    this.canvas.height = this.faviconSize;
    const sd = this.faviconSize - this.badgeSize;
    const sd2 = sd / 2;
    this.offset = {
      n:  {x: sd2, y: 0 },
      e:  {x: sd, y: sd2},
      s:  {x: sd2, y: sd},
      w:  {x: 0, y: sd2},
      nw: {x: 0, y: 0},
      ne: {x: sd, y: 0},
      sw: {x: 0, y: sd},
      se: {x: sd, y: sd},
    }[this.position];
  }

  // Public functions / methods:

  update() {
    this._value = Math.min(99, parseInt(this._value, 10));
    if (this.img) {
      this._draw();
      if (this.onChange) this.onChange.call(this);
    } else {
      this.img = new Image();
      this.img.addEventListener("load", () => {
        this._setup();
        this._draw();
        if (this.onChange) this.onChange.call(this);
      });
      this.img.src = this.src;
    }
  }

  get dataURL() {
    return this.canvas.toDataURL();
  }

  get value() {
    return this._value;
  }

  set value(val) {
    this._value = val;
    this.update();
  }
}
Yvoneyvonne answered 14/1, 2021 at 14:22 Comment(0)
I
1

SVG files are valid as favicons and since SVG is structured markup, you can simply replace a text element inside the markup in the same way you would replace text content in a DOM-element.

Iapetus answered 29/5 at 12:59 Comment(0)
T
0

Modified version of @Roko C.Buljan to change all link icons, and have a percent radius

/**
 * Add notification badge (pill) to favicon in browser tab
 * @url stackoverflow.com/questions/65719387/
 */
class Badger {
  constructor(options) {
    Object.assign(
      this, {
        backgroundColor: "#f00",
        color: "#fff",
        size: 0.6,      // 0..1 (Scale in respect to the favicon image size)
        position: "se", // Position inside favicon "n", "e", "s", "w", "ne", "nw", "se", "sw"
        radius: 0.10,      // Border radius, better used as % value + 4px base
        src: "",        // Favicon source (dafaults to the <link> icon href),
        srcs: false,
        onChange() {},
      },
      options
    );
    this.canvas = document.createElement("canvas");
    this.ctx = this.canvas.getContext("2d");

    this.src = "";
    this.img = "";
    this.srcs = this.srcs || this.faviconELs;
  }

  faviconELs = document.querySelectorAll("link[rel$=icon]");

  _drawIcon() {
    this.ctx.clearRect(0, 0, this.faviconSize, this.faviconSize);
    this.ctx.drawImage(this.img, 0, 0, this.faviconSize, this.faviconSize);
  }

  _drawShape() {
    const r = Math.floor(this.faviconSize * this.radius) + 4;
    const xa = this.offset.x;
    const ya = this.offset.y;
    const xb = this.offset.x + this.badgeSize;
    const yb = this.offset.y + this.badgeSize;
    this.ctx.beginPath();
    this.ctx.moveTo(xb - r, ya);
    this.ctx.quadraticCurveTo(xb, ya, xb, ya + r);
    this.ctx.lineTo(xb, yb - r);
    this.ctx.quadraticCurveTo(xb, yb, xb - r, yb);
    this.ctx.lineTo(xa + r, yb);
    this.ctx.quadraticCurveTo(xa, yb, xa, yb - r);
    this.ctx.lineTo(xa, ya + r);
    this.ctx.quadraticCurveTo(xa, ya, xa + r, ya);
    this.ctx.fillStyle = this.backgroundColor;
    this.ctx.fill();
    this.ctx.closePath();
  }

  _drawVal() {
    const margin = (this.badgeSize * 0.18) / 2;
    this.ctx.beginPath();
    this.ctx.textBaseline = "middle";
    this.ctx.textAlign = "center";
    this.ctx.font = `bold ${this.badgeSize * 0.82}px Arial`;
    this.ctx.fillStyle = this.color;
    this.ctx.fillText(this.value, this.badgeSize / 2 + this.offset.x, this.badgeSize / 2 + this.offset.y + margin);
    this.ctx.closePath();
  }

  _drawFavicon() {
    this.src.setAttribute("href", this.dataURL);
  }

  _draw() {
    this._drawIcon();
    if (this.value) this._drawShape();
    if (this.value) this._drawVal();
  }

  _setup(el) {
    this.img = el.img;
    this.src = el.src;
    
    this.faviconSize = this.img.naturalWidth;
    this.badgeSize = this.faviconSize * this.size;
    this.canvas.width = this.faviconSize;
    this.canvas.height = this.faviconSize;
    const sd = this.faviconSize - this.badgeSize;
    const sd2 = sd / 2;
    this.offset = {
      n:  {x: sd2, y: 0 },
      e:  {x: sd, y: sd2},
      s:  {x: sd2, y: sd},
      w:  {x: 0, y: sd2},
      nw: {x: 0, y: 0},
      ne: {x: sd, y: 0},
      sw: {x: 0, y: sd},
      se: {x: sd, y: sd},
    }[this.position];
  }

  // Public functions / methods:
  imgs = [];
  updateAll()
  {
    this._value = Math.min(99, parseInt(this._value, 10));
    var self = this;

    if (this.imgs.length) {
        this.imgs.forEach(function(img) {
            self._setup(img);
            self._draw();
            self._drawFavicon();
        })
        if (this.onChange) this.onChange.call(this);
    } else {
        // load all
        this.srcs.forEach(function(src) {
          var img = {};
          img.img = new Image();
          img.img.addEventListener("load", () => {
            self._setup(img);
            self._draw();
            self._drawFavicon();
            if (self.onChange) self.onChange.call(self);
          });
          img.src = src;
          img.img.src = src.getAttribute("href");
          self.imgs.push(img);
        })
    }
  }

  get dataURL() {
    return this.canvas.toDataURL();
  }

  get value() {
    return this._value;
  }

  set value(val) {
    this._value = val;
    this.updateAll();
  }
}
Thomasthomasa answered 17/6 at 15:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.