Skip to content

Instantly share code, notes, and snippets.

@bebetor
Created February 26, 2022 06:37
Show Gist options
  • Save bebetor/a64f8b0cae39b77b0c96cf06631be96c to your computer and use it in GitHub Desktop.
Save bebetor/a64f8b0cae39b77b0c96cf06631be96c to your computer and use it in GitHub Desktop.
Lotsa Notifications
<svg display="none">
<symbol id="clock" viewBox="0 0 32 32" >
<circle r="15" cx="16" cy="16" fill="none" stroke="currentColor" stroke-width="2" />
<polyline points="16,7 16,16 23,16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</symbol>
<symbol id="error" viewBox="0 0 32 32" >
<circle r="15" cx="16" cy="16" fill="none" stroke="hsl(13,90%,55%)" stroke-width="2" />
<line x1="10" y1="10" x2="22" y2="22" stroke="hsl(13,90%,55%)" stroke-width="2" stroke-linecap="round" />
<line x1="22" y1="10" x2="10" y2="22" stroke="hsl(13,90%,55%)" stroke-width="2" stroke-linecap="round" />
</symbol>
<symbol id="message" viewBox="0 0 32 32" >
<polygon points="1,6 31,6 31,26 1,26" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<polyline points="1,6 16,18 31,6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</symbol>
<symbol id="success" viewBox="0 0 32 32" >
<circle r="15" cx="16" cy="16" fill="none" stroke="hsl(93,90%,40%)" stroke-width="2" />
<polyline points="9,18 13,22 23,12" fill="none" stroke="hsl(93,90%,40%)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</symbol>
<symbol id="up" viewBox="0 0 32 32" >
<circle r="15" cx="16" cy="16" fill="none" stroke="currentColor" stroke-width="2" />
<polyline points="11,15 16,10 21,15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<line x1="16" y1="10" x2="16" y2="22" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</symbol>
<symbol id="warning" viewBox="0 0 32 32" >
<polygon points="16,1 31,31 1,31" fill="none" stroke="hsl(33,90%,55%)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<line x1="16" y1="12" x2="16" y2="20" stroke="hsl(33,90%,55%)" stroke-width="2" stroke-linecap="round" />
<line x1="16" y1="25" x2="16" y2="25" stroke="hsl(33,90%,55%)" stroke-width="3" stroke-linecap="round" />
</symbol>
</svg>
window.addEventListener("DOMContentLoaded",() => {
const nc = new NotificationCenter();
});
class NotificationCenter {
constructor() {
this.items = [];
this.itemsToKill = [];
this.messages = NotificationMessages();
this.killTimeout = null;
this.spawnNotes(3);
}
spawnNote() {
const id = this.random(0,2**32,true).toString(16);
const draw = this.random(0,this.messages.length - 1,true);
const message = this.messages[draw];
const note = new Notification({
id: `note-${id}`,
icon: message.icon,
title: message.title,
subtitle: message.subtitle,
actions: message.actions
});
const transY = 100 * this.items.length;
note.el.style.transform = `translateY(${transY}%)`;
note.el.addEventListener("click",this.killNote.bind(this,note.id));
this.items.push(note);
}
spawnNotes(amount) {
let count = typeof amount === "number" ? amount : this.random(1,5,true);
while (count--)
this.spawnNote();
}
killNote(id,e) {
const note = this.items.find(item => item.id === id);
const tar = e.target;
if (note && tar.getAttribute("data-dismiss") === id) {
note.el.classList.add("notification--out");
this.itemsToKill.push(note);
clearTimeout(this.killTimeout);
this.killTimeout = setTimeout(() => {
this.itemsToKill.forEach(itemToKill => {
document.body.removeChild(itemToKill.el);
const left = this.items.filter(item => item.id !== itemToKill.id);
this.items = [...left];
});
this.itemsToKill = [];
if (!this.items.length)
this.spawnNotes();
else
this.spawnNotes(this.random(0,1,true));
this.shiftNotes();
},note.killTime);
}
}
shiftNotes() {
this.items.forEach((item,i) => {
const transY = 100 * i;
item.el.style.transform = `translateY(${transY}%)`;
});
}
random(min,max,round = false) {
const percent = crypto.getRandomValues(new Uint32Array(1))[0] / 2**32;
const relativeValue = (max - min) * percent;
return min + (round === true ? Math.round(relativeValue) : +relativeValue.toFixed(2));
}
}
class Notification {
constructor(args) {
this.args = args;
this.el = null;
this.id = null;
this.killTime = 300;
this.init(args);
}
init(args) {
const {id,icon,title,subtitle,actions} = args;
const block = "notification";
const parent = document.body;
const xmlnsSVG = "http://www.w3.org/2000/svg";
const xmlnsUse = "http://www.w3.org/1999/xlink";
const note = this.newEl("div");
note.id = id;
note.className = block;
parent.insertBefore(note,parent.lastElementChild);
const box = this.newEl("div");
box.className = `${block}__box`;
note.appendChild(box);
const content = this.newEl("div");
content.className = `${block}__content`;
box.appendChild(content);
const _icon = this.newEl("div");
_icon.className = `${block}__icon`;
content.appendChild(_icon);
const iconSVG = this.newEl("svg",xmlnsSVG);
iconSVG.setAttribute("class",`${block}__icon-svg`);
iconSVG.setAttribute("role","img");
iconSVG.setAttribute("aria-label",icon);
iconSVG.setAttribute("width","32px");
iconSVG.setAttribute("height","32px");
_icon.appendChild(iconSVG);
const iconUse = this.newEl("use",xmlnsSVG);
iconUse.setAttributeNS(xmlnsUse,"href",`#${icon}`);
iconSVG.appendChild(iconUse);
const text = this.newEl("div");
text.className = `${block}__text`;
content.appendChild(text);
const _title = this.newEl("div");
_title.className = `${block}__text-title`;
_title.textContent = title;
text.appendChild(_title);
if (subtitle) {
const _subtitle = this.newEl("div");
_subtitle.className = `${block}__text-subtitle`;
_subtitle.textContent = subtitle;
text.appendChild(_subtitle);
}
const btns = this.newEl("div");
btns.className = `${block}__btns`;
box.appendChild(btns);
actions.forEach(action => {
const btn = this.newEl("button");
btn.className = `${block}__btn`;
btn.type = "button";
btn.setAttribute("data-dismiss",id);
const btnText = this.newEl("span");
btnText.className = `${block}__btn-text`;
btnText.textContent = action;
btn.appendChild(btnText);
btns.appendChild(btn);
});
this.el = note;
this.id = note.id;
}
newEl(elName,NSValue) {
if (NSValue)
return document.createElementNS(NSValue,elName);
else
return document.createElement(elName);
}
}
function NotificationMessages() {
return [
{
icon: "error",
title: "Oh No",
subtitle: "Something really bad happened.",
actions: ["Close"]
},
{
icon: "error",
title: "Error",
subtitle: "The operation completed successfully.",
actions: ["OK"]
},
{
icon: "error",
title: "Critical Error",
subtitle: "An error has occurred while trying to display an error notification.",
actions: ["OK"]
},
{
icon: "warning",
title: "Reminder",
subtitle: "You will receive more notifications.",
actions: ["Close"]
},
{
icon: "warning",
title: "Failed to Save Changes",
actions: ["Retry","Cancel"]
},
{
icon: "warning",
title: "Download Failed",
actions: ["Retry","Cancel"]
},
{
icon: "warning",
title: "Cannot Send Mail",
subtitle: "The message was rejected by the server because it is too large.",
actions: ["OK"]
},
{
icon: "warning",
title: "Disk Not Ejected Properly",
subtitle: "Eject “CopyThisFloppy” before disconnecting or turning it off.",
actions: ["Close"]
},
{
icon: "warning",
title: "Notifications",
subtitle: "Notifications may include alerts, sounds, and icon badges.",
actions: ["Don’t Allow","Allow"]
},
{
icon: "success",
title: "Changes Saved",
actions: ["OK"]
},
{
icon: "success",
title: "Download Complete",
actions: ["OK"]
},
{
icon: "success",
title: "Yippee",
subtitle: "Nothing bad happened.",
actions: ["OK"]
},
{
icon: "message",
title: "Mail Password Required",
subtitle: "Enter your password for user@domain.com.",
actions: ["Close","Continue"]
},
{
icon: "message",
title: "Mail",
subtitle: "You have 10 new messages.",
actions: ["Read","Dismiss"]
},
{
icon: "clock",
title: "Coffee Break",
subtitle: "In 5 minutes",
actions: ["Close","Snooze"]
},
{
icon: "clock",
title: "Muffin Time",
subtitle: "12:30 PM",
actions: ["Close","Snooze"]
},
{
icon: "clock",
title: "Hammer Time",
subtitle: "In 2 minutes",
actions: ["Close","Snooze"]
},
{
icon: "up",
title: "Upgrade Available",
subtitle: "Enjoy the latest technologies and refinements to your favorite apps.",
actions: ["Install","Details"]
},
{
icon: "up",
title: "Upgrade Waiting",
subtitle: "Get it now, or it won’t be long until you are far behind everyone else.",
actions: ["Install","Details"]
},
{
icon: "up",
title: "Upgrade Now",
subtitle: "The current version will soon be obsolete. What are you waiting for?",
actions: ["Install","Details"]
}
];
}
* {
border: 0;
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--hue: 223;
--bg: hsl(var(--hue),10%,90%);
--fg: hsl(var(--hue),10%,10%);
--transDur: 0.15s;
font-size: calc(16px + (24 - 16) * (100vw - 320px) / (1280 - 320));
}
body,
button {
color: var(--fg);
font: 1em/1.5 "DM Sans", "Helvetica Neue", Helvetica, sans-serif;
}
body {
background-color: var(--bg);
height: 100vh;
display: grid;
place-items: center;
transition: background-color var(--transDur);
}
.notification {
padding-bottom: 0.75em;
position: fixed;
top: 1.5em;
right: 1.5em;
width: 18.75em;
max-width: calc(100% - 3em);
transition: transform 0.15s ease-out;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.notification__box,
.notification__content,
.notification__btns {
display: flex;
}
.notification__box,
.notification__content {
align-items: center;
}
.notification__box {
animation: flyIn 0.3s ease-out;
background-color: hsl(0,0%,100%);
border-radius: 0.75em;
box-shadow: 0 0.5em 1em hsla(var(--hue),10%,10%,0.1);
height: 4em;
transition:
background-color var(--transDur),
color var(--transDur);
}
.notification--out .notification__box {
animation: flyOut 0.3s ease-out forwards;
}
.notification__content {
padding: 0.375em 1em;
width: 100%;
height: 100%;
}
.notification__icon {
flex-shrink: 0;
margin-right: 0.75em;
width: 2em;
height: 2em;
}
.notification__icon-svg {
width: 100%;
height: auto;
}
.notification__text {
line-height: 1.333;
}
.notification__text-title {
font-size: 0.75em;
font-weight: bold;
}
.notification__text-subtitle {
font-size: 0.6em;
opacity: 0.75;
}
.notification__btns {
box-shadow: -1px 0 0 hsla(var(--hue),10%,10%,0.15);
flex-direction: column;
flex-shrink: 0;
min-width: 4em;
height: 100%;
transition: box-shadow var(--transDur);
}
.notification__btn {
background-color: transparent;
box-shadow: 0 0 0 hsla(var(--hue),10%,10%,0.5) inset;
font-size: 0.6em;
line-height: 1;
font-weight: 500;
height: 100%;
padding: 0 0.5rem;
transition:
background-color var(--transDur),
color var(--transDur);
-webkit-appearance: none;
appearance: none;
-webkit-tap-highlight-color: transparent;
}
.notification__btn-text {
display: inline-block;
pointer-events: none;
}
.notification__btn:first-of-type {
border-radius: 0 0.75rem 0 0;
}
.notification__btn:last-of-type {
border-radius: 0 0 0.75rem 0;
}
.notification__btn:only-child {
border-radius: 0 0.75rem 0.75rem 0;
}
.notification__btn + .notification__btn {
box-shadow: 0 -1px 0 hsla(var(--hue),10%,10%,0.15);
font-weight: 400;
}
.notification__btn:active,
.notification__btn:focus {
background-color: hsl(var(--hue),10%,95%);
}
.notification__btn:focus {
outline: transparent;
}
@supports selector(:focus-visible) {
.notification__btn:focus {
background-color: transparent;
}
.notification__btn:focus-visible,
.notification__btn:active {
background-color: hsl(var(--hue),10%,95%);
}
}
/* Dark theme */
@media (prefers-color-scheme: dark) {
:root {
--bg: hsl(var(--hue),10%,10%);
--fg: hsl(var(--hue),10%,90%);
}
.notification__box {
background-color: hsl(var(--hue),10%,30%);
}
.notification__btns {
box-shadow: -1px 0 0 hsla(var(--hue),10%,90%,0.15);
}
.notification__btn + .notification__btn {
box-shadow: 0 -1px 0 hsla(var(--hue),10%,90%,0.15);
}
.notification__btn:active,
.notification__btn:focus {
background-color: hsl(var(--hue),10%,35%);
}
@supports selector(:focus-visible) {
.notification__btn:focus {
background-color: transparent;
}
.notification__btn:focus-visible,
.notification__btn:active {
background-color: hsl(var(--hue),10%,35%);
}
}
}
/* Animations */
@keyframes flyIn {
from {
transform: translateX(calc(100% + 1.5em));
}
to {
transform: translateX(0);
}
}
@keyframes flyOut {
from {
transform: translateX(0);
}
to {
transform: translateX(calc(100% + 1.5em));
}
}
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&amp;display=swap" rel="stylesheet" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment