They just keep on popping up. Close one, more may take its place!
A Pen by Jon Kantner on CodePen.
<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> |
They just keep on popping up. Close one, more may take its place!
A Pen by Jon Kantner on CodePen.
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&display=swap" rel="stylesheet" /> |