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 [email protected].", | |
| 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" /> |