Всплывающие уведомления (CSS3 & JS)

Существует множество способов оперативно уведомить пользователя и сфокусировать его внимание на важном контенте, начиная от обычных алертов в JavaScript и заканчивая модальными окнами. Вниманию читателя представляются простые, удобные и красивые всплывающие уведомления на CSS3 и JS (именуемые в англоязычной среде «CSS alerts»), которые фиксируются в верхней части экрана и могут быть использованы для отображения различных состояний — ошибки, успешного действия, информации или предупреждения. Алерты полностью адаптивны и работают во всех современных браузерах.

Уведомления «под капотом»

На просторах сети встречается большое количество скриптов и техник создания всплывающих уведомлений: алерты могут быть реализованы на «ванильном» JS, посредством плагинов JQuery и даже на чистом CSS3 с применением псевдокласса :target. Представленный в статье способ создания уведомлений классически совмещает обычный JS и CSS3, при этом алерты являются адаптивными, красиво выделяются и закрепляются в верхней части экрана, позволяют выводить как обычный текст, так и HTML, исчезают по истечению заданного времени и незначительно модифицируют DOM динамическим добавлением пары элементов в конец документа лишь на время исполнения функции и существования самого алерта.

HTML-разметка

Указанный HTML будет автоматически встраиваться перед закрывающим тегом </body> при вызове алерта. За определение состояний уведомлений отвечает атрибут data-type — этот селектор используется для соответствующего CSS-оформления. У алерта может быть четыре состояния (разумеется, при желании их можно дополнить): info — информационное уведомление, warn — предупреждение, success — оповещение об успешном действии и error — сообщение об ошибке.

<body>
  <!-- ... page content -->
  <div id="notes">
    <div class="note-item" data-type="info" role="alert">
      <div class="note-item-text">Announcement</div>
      <button class="note-item-btn" role="button" aria-label="Скрыть"></button>
    </div>
  </div>
</body>

(S)CSS-оформление

Адаптивность уведомлений обеспечивается использованием относительных единиц. Все поля (margin) и отступы (padding) внутри алерта заданы в em, следовательно, зависят от размера шрифта, установленного для тела документа. Сам шрифт алертов и все его параметры так же наследуются от <body>. Стандартная ширина уведомлений составляет половину viewport — 50vw. При этом, чтобы избежать излишнего «растягивания» на больших разрешениях экрана для уведомлений установлена максимальная ширина в 20em, а для маленьких — минимальные 75vw. Таким образом, алерты всегда будут гармонично «вписываться» в экран любого устройства.

Для большего визуального эффекта и выделения на фоне прочих элементов к уведомлениям с состоянием ошибки или предупреждения добавлена «трясущая» анимация:

#notes {
  position: fixed;
  top: 1em;
  width: 100%;
  cursor: default;
  transition: height .45s ease-in-out;
  -webkit-transition: height .45s ease-in-out;
  pointer-events: none;
  z-index: 1;
  
  .note-item { 
    max-height: 12em;
    opacity: 1;
    will-change: opacity, transform;
    transition: all .2s linear;
    -webkit-transition: all .2s linear;
    backface-visibility: hidden;
    -webkit-backface-visibility: hidden;
    -moz-backface-visibility: hidden;
    -ms-backface-visibility: hidden;
    width: 50vw;
    -webkit-touch-callout: none;
    user-select: none;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    -o-user-select: none;
    z-index: 2;
    pointer-events: auto;
    display: -webkit-box;
    display: -webkit-flex;
    display: -moz-flex;
    display: -ms-flex;
    display: -ms-flexbox;
    display: flex;
    -webkit-align-content: flex-start;
    -ms-flex-line-pack: start;
    align-content: flex-start;
    -webkit-box-align: start;
    -ms-flex-align: start;
    -webkit-align-items: flex-start;
    -moz-align-items: flex-start;
    align-items: flex-start;
    -webkit-align-content :flex-start;
    -ms-flex-line-pack: start;
    align-content: flex-start;
    
    @media all and (max-width: 30em) {
      width: 75vw;
      max-width: none;
    } 
    
    max-width: 20em;
    font: inherit;
    line-height: 1.25em;
    color: #fff;
    margin: 0 auto 1em auto;
    transform: translateZ(0);
    -moz-transform: translateZ(0);
    -ms-transform: translateZ(0);
    -webkit-transform: translateZ(0);
    padding: .75em 1em;

    &[data-show="false"] {
      pointer-events: none;
      opacity: 0 !important;
      max-height: 0 !important;
      margin-bottom: 0 !important;
    }
    
    &[data-type="info"] {
      background-color: rgba(#375e97,72%);
    }

    &[data-type="warn"] {
      background-color: rgba(#EBAC00,72%);
      animation: shake .9s cubic-bezier(.36,.07,.19,.97) both;
      -webkit-animation: shake .9s cubic-bezier(.36,.07,.19,.97) both;
    }

    &[data-type="error"] {
      background-color: rgba(#fb6542,72%);
      animation: shake 0.54s cubic-bezier(.36,.07,.19,.97) both;
      -webkit-animation: shake 0.54s cubic-bezier(.36,.07,.19,.97) both;
    }

    &[data-type="success"] {
      background-color: rgba(#3f681c,72%);
    }
    
    .note-item-text {
      flex: auto;
      -webkit-flex: auto;
      -moz-flex: auto;
      -ms-flex: auto;
      padding-right: .5em;
      max-width: calc(100% - 1.25em);
      max-width: -webkit-calc(100% - 1.25em);
    }
    
    .note-item-btn {
      width: 1.25em;
      height: 1.25em;
      cursor: pointer;
      background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48c3ZnIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiB4bWw6c3BhY2U9InByZXNlcnZlIiBmaWxsPSIjZmZmIj48cGF0aCBkPSJNMTguMyw1LjcxTDE4LjMsNS43MWMtMC4zOS0wLjM5LTEuMDItMC4zOS0xLjQxLDBMMTIsMTAuNTlMNy4xMSw1LjdjLTAuMzktMC4zOS0xLjAyLTAuMzktMS40MSwwbDAsMCBjLTAuMzksMC4zOS0wLjM5LDEuMDIsMCwxLjQxTDEwLjU5LDEyTDUuNywxNi44OWMtMC4zOSwwLjM5LTAuMzksMS4wMiwwLDEuNDFoMGMwLjM5LDAuMzksMS4wMiwwLjM5LDEuNDEsMEwxMiwxMy40MWw0Ljg5LDQuODkgYzAuMzksMC4zOSwxLjAyLDAuMzksMS40MSwwbDAsMGMwLjM5LTAuMzksMC4zOS0xLjAyLDAtMS40MUwxMy40MSwxMmw0Ljg5LTQuODlDMTguNjgsNi43MywxOC42OCw2LjA5LDE4LjMsNS43MXoiLz48L3N2Zz4=) no-repeat 0 0 / contain;
      transition: opacity .2s;
      -webkit-transition: opacity .2s;
      &:hover { opacity: .6; }
    }

  }
  
}

@keyframes shake {
  10%, 90% {
    transform: translate3d(-1px, 0, 0);
    -webkit-transform: translate3d(-1px, 0, 0);
    -ms-transform: translate3d(-1px, 0, 0);
  }
  
  20%, 80% {
    transform: translate3d(2px, 0 0);
    -webkit-transform: translate3d(2px, 0, 0);
    -ms-transform: translate3d(2px,0, 0);    
  }

  30%, 50%, 70% {
    transform: translate3d(-4px, 0, 0);
    -webkit-transform: translate3d(-4px, 0, 0);
    -ms-transform: translate3d(-4px, 0, 0);
  }

  40%, 60% {
    transform: translate3d(4px, 0, 0);
    -webkit-transform: translate3d(4px, 0, 0);
    -ms-transform: translate3d(4px, 0, 0);
  }
}

@-webkit-keyframes shake {
  10%, 90% {
    -webkit-transform: translate3d(-1px, 0, 0);
  }
  
  20%, 80% {
    -webkit-transform: translate3d(2px, 0, 0);
  }

  30%, 50%, 70% {
    -webkit-transform: translate3d(-4px, 0, 0)t;
  }

  40%, 60% {
    -webkit-transform: translate3d(4px, 0, 0);
  }
}

Блоки с алертами существуют только указанное время, но их родительский элемент создается при первом запуске функции и остается в DOM-дереве. При желании он может быть скрыт, когда не задействован:

#notes {
  &:empty { display: none; }
}

JavaScript

Функция вызова уведомлений принимает единственный аргумент — объект, содержащий предварительные настройки. В теле функции значение этих настроек устанавливается по умолчанию:

{
  callback: false,           // функция, которая будет вызываться после скрытия алерта
  content: "Текст или HTML", // содержимое уведомления (допускается HTML)
  time: 5,                   // время, на которое уведомление появляется (секунды)
  type: "info"               // тип уведомления: info, error, success, warn
}

Уведомление генерируется по следующему принципу: при первом вызове в <body> после всех дочерних элементов создаётся родительский блок (#notes), куда, в свою очередь, вставляется само уведомление (.note-item). По истечению заданного времени оно автоматически исчезает и удаляется из DOM. Создание и удаление уведомлений происходит с небольшой задержкой, необходимой для обеспечения CSS-переходов. При этом одновременно может быть выведено несколько алертов подряд: они автоматически сместятся вниз, а при вертикальном заполнении экрана начнут по очереди удаляться, чтобы освободить место новым.

Последующий вызов функции не отражается на родительском элементе #notes. При желании его можно скрыть указанным выше способом.

/**
 * Анонимная самовызывающаяся функция-обертка
 * @param {document} d - получает документ
 */
!function(d) {

  "use strict";

  /**
   * Основная функция.
   * @param {Object} [settings] - предвартиельные настройки
   */
  window.note = function(settings) {

    /**
     * Настройки по умолчанию
     */
    settings = Object.assign({},{
      callback:    false,
      content:     "",
      time:        4.5,
      type:        "info"
    }, settings);

    if(!settings.content.length) return;

    /**
     * Функция создания элементов
     * @param {String} name - название DOM-элемента
     * @param {Object} attr - объект с атрибутами
     * @param {Object} append - DOM-элемент, в который будет добавлен новый узел
     * @param {String} [content] - контент DOM-элемента
     */
    var create = function(name, attr, append, content) {
      var node = d.createElement(name);
      for(var val in attr) { if(attr.hasOwnProperty(val)) node.setAttribute(val, attr[val]); }
      if(content) node.insertAdjacentHTML("afterbegin", content);
      append.appendChild(node);
      if(node.classList.contains("note-item-hidden")) node.classList.remove("note-item-hidden");
      return node;
    };

    /**
     * Генерация элементов
     */
    var noteBox = d.getElementById("notes") || create("div", { "id": "notes" }, d.body);
    var noteItem = create("div", {
        "class": "note-item",
        "data-show": "false",
        "role": "alert",
        "data-type": settings.type
      }, noteBox),
      noteItemText = create("div", { "class": "note-item-text" }, noteItem, settings.content),
      noteItemBtn = create("button", {
        "class": "note-item-btn",
        "type": "button",
        "aria-label": "Скрыть"
      }, noteItem);

    /**
     * Функция проверки видимости алерта во viewport
     * @returns {boolean}
     */
    var isVisible = function() {
      var coords = noteItem.getBoundingClientRect();
      return (
        coords.top >= 0 &&
        coords.left >= 0 &&
        coords.bottom <= (window.innerHeight || d.documentElement.clientHeight) && 
        coords.right <= (window.innerWidth || d.documentElement.clientWidth) 
      );
    };
    
    /**
     * Функция удаления алертов
     * @param {Object} [el] - удаляемый алерт
     */
    var remove = function(el) {
      el = el || noteItem;
      el.setAttribute("data-show","false");
      window.setTimeout(function() {
        el.remove();
      }, 250);
      if(settings.callback) settings.callback(); // callback
    };

    /**
     * Удаление алерта по клику на кнопку
     */
    noteItemBtn.addEventListener("click", function() { remove(); });

    /**
     * Визуальный вывод алерта
     */
    window.setTimeout(function() {
      noteItem.setAttribute("data-show","true");
    }, 250);


    /**
     * Проверка видимости алерта и очистка места при необходимости
     */
    if(!isVisible()) remove(noteBox.firstChild);

    /**
     * Автоматическое удаление алерта спустя заданное время
     */
    window.setTimeout(remove, settings.time * 1000);

  };

}(document);

Использовать функцию достаточно легко. Так, например, на 15 секунд вызывается алерт, информирующий об ошибке:

note({
  content: "Something going <b>wrong</b>",
  type: "error",
  time: 15
});

Пример всплывающих уведомлений

Для правильной работы скрипта в браузере Internet Explorer 10+ для методов childNode.remove() и Object.assign() следует добавить полифиллы. Важный нюанс: если JS без проблем выполняется в старых браузерах, то с CSS3 не всё так однозначно: анимации и единицы vw поддерживаются не везде.

В заключение

Уведомления, как и всплывающие подсказки, являются универсальным способом вывести лаконичную информацию и обратить на нее внимание посетителя. Представленная техника создания так называемых «CSS alerts» удобна для использования на сайтах с адаптивным дизайном и не требует подключения сторонних JS-библиотек. Кроме того, скрипт может быть усовершенствован под любые потребности, например, возможностью установить позиционирование относительно страницы или добавлением к сообщениям различных иконок.