Всем привет! Я в веб-разработке не так давно. Сейчас я пишу свой сайт, который будет выступать в качестве моего портфолио и, возможно, даже целого проекта. При добавлении модального окна для авторизации на свой сайт, я подумал, а что будет, если публиковать подобные модульные вещи, чтобы любой человек мог их скопировать и не думать о них, а использовать в разработке. То есть создать готовый модуль для встраивания. Мне, как разработчику было бы удобно использовать готовое решение, тем более написанное мною, да и делиться опытом - дело приятное :)

Для тех, кому надо сразу код и вид окна.

NB

сам сайт скрыт для сохранения интеллектуальной собственности :)

А теперь пошаговое создание:

<div class="modalBackground">
    <div class="modalActive">
        <div class="modalClose">
            <img src="modalCross.svg" />
        </div>
        <div class="modalWindow"></div>
    </div>
</div>

Здесь представлена несложная разметка для модального окна.

Перед разъяснением элементов и их стилей, мы укажем скрытие ползунка прокрутки для body, которое нам будет мешать:

/* убираем нижний ползунок прокрутки */
body {
    overflow-x: hidden;
}

Давайте поясню, что есть что:

  • modalBackground - это фон, расположенный за модальным окном. Через него мы будем видеть наш сайт { background: rgba(0, 0, 0, 0.8); }, в то время как основной фокус будет на модальном окне. Для корректного расположения, чтобы наш фон покрывал весь сайт - мы делаем его с положением { position: fixed } и размерами на весь экран { width: 100%; height : 100%; }. А для наложения поверх других окон или элементов сайта - { z-index: 1 }. Стоит отметить, что изначально мы не можем видеть наше окно { display: none }. Для отображения возможности нажатия на фон - делаем указатель через { cursor: pointer; }

/* фон нашего модального окна */
.modalBackground {
    display: none;
    background: rgba(0, 0, 0, 0.8);
    position: fixed;
    width: 100%;
    height: 100%;
    cursor: pointer;
    /* указываем z-индекс для корректного наслаивания */
    z-index: 1;
}
  • modalActive - непосредственно наше модальное окно. Его размеры { width: 350px; height : 495px; } я подбирал исходя из удобства верстки для маленьких экранов, так что размеры можно указать свои. Мы позиционируем наше модальное окно по центру { position: absolute; top: calc(50% - 250px); left: calc(50% - 175px); }. Делаем рамки закругления в 10 пикселей { border-radius: 10px }. Курсор для удобства делаем стандартным { cursor: default }. Свойства background-color и padding в данном случае ни на что не влияют, они используются для фона нашего окошечка внутри и для внутренних отступов нашего окошечка соответственно. Поэтому их можно ставить на свой вкус и цвет :)

/* позиционирование самого модального окна */
.modalActive {
    position: absolute;
    width: 350px;
    height: 495px;
    top: calc(50% - 250px);
    left: calc(50% - 175px);
    border-radius: 10px;
    background-color: rgb(255, 255, 255);
    cursor: default;
    padding: 40px 20px;
}
  • modalClose - это наш крестик. Я использовал два варианта выхода из модального окна, кому как удобнее - через нажатие на фон, либо через нажатие на крестик (этот вариант предпочтительнее для мобильных устройств из-за маленького экрана, по сравнению с монитором ноутбука/компьютера/планшета). Для крестика соответственно используется тег modalCross с указанием в img пути к картинке-крестика. Свойство font-family используется для выбора семейства шрифта, здесь оно не играет роли (на ваш вкус и цвет).

/* кнопочка закрытия модального окна */
.modalClose {
    font-family: var(--font-regular);
    position: absolute;
    right: 5px;
    top: 5px;
    width: 30px;
    height: 30px;
    cursor: pointer;
}

/* сама картинка кнопочки закрытия */
.modalClose img {
    margin: 3px;
    width: 24px;
    height: 24px;
}
  • modalWindow - внутри него располагается содержимое нашего окна, для удобной верстки внутреннего содержимого устанавливаем относительное позиционирование относительно нашего модального окна { position: relative }. Какое оно будет - зависит только от вас :)

/* делаем позиционирование внутренних элементов относительно модального окна */
.modalWindow {
    position: relative;
}

Теперь приступим к части оживления нашего окна с помощью жабаскрипта JS !

Прежде всего нам надо получить все элементы, с которыми нам предстоит работать:

// устанавливаем триггер для модального окна (название можно изменить)
const modalTrigger = document.getElementsByClassName("trigger")[0];

// получаем ширину отображенного содержимого и толщину ползунка прокрутки
const windowInnerWidth = document.documentElement.clientWidth;
const scrollbarWidth = parseInt(window.innerWidth) - parseInt(document.documentElement.clientWidth);

// привязываем необходимые элементы
const bodyElementHTML = document.getElementsByTagName("body")[0];
const modalBackground = document.getElementsByClassName("modalBackground")[0];
const modalClose = document.getElementsByClassName("modalClose")[0];
const modalActive = document.getElementsByClassName("modalActive")[0];

Стоит отметить, что при использовании метода getElementsByClassName мы будем получать массив всех элементов. Поэтому для корректного обращения к конкретному элементу - мы указываем индекс получаемого элемента. А так как наши массивы состоят из одного элемента - индекс во всех случаях равен [0].

Переменная modalTrigger - содержит наш триггер, при нажатии на который у нас будет появляться наше модальное окно.

Если наш сайт длинный и у нас появляется ползунок прокрутки - мы получаем его длину и записываем в scrollbarWidth. Давайте тут поподробнее. Мы изначально получаем ширину видимой части сайта через свойство clientWidth. Ширина ползунка прокрутки - разность между шириной окна внутри и шириной отображаемого содержимого (то, что под ползунком - не является отображаемым содержимым). Для корректности мы преобразовываем полученные значения через функции parseInt.

Далее мы привязываем необходимые элементы к переменным в соответствии с названием их классов, для тега body мы используем метод getElementsByTagName с указанием индекса получаемого элемента, то есть [0]. Смысл такой же, как и при getElementsByClassName. Я заметил простую вещь - если читаешь название методов, то понимаешь что они тебе дают :). В нашем случаем мы получаем множество элементов (окончание s в слове Elements), поэтому мы и работаем с массивом. А в методе getElementById мы получаем один элемент (о чем свидетельствует отсутствие окончания s в слове Element).

После инициализации рабочих переменных - мы корректируем положение body при появлении ползунка прокрутки, то есть наш ползунок будет накладываться поверх нашего сайта, а не сдвигать его, как это обычно бывает (можете проверить сами):

// функция для корректировки положения body при появлении ползунка прокрутки
function bodyMargin() {
    bodyElementHTML.style.marginRight = "-" + scrollbarWidth + "px";
}

// при длинной странице - корректируем сразу
bodyMargin();

Механизм корректировки прост: в случае появления ползунка прокрутки - мы делаем отступ body через свойство margin-right на отрицательную величину ширины ползунка прокрутки. При первой загрузки страницы мы вызываем эту функцию, чтобы наше содержимое сразу же позиционировалось корректно, несмотря на наличие ползунка прокрутки. Вообще, позиционирование с учетом ползунка прокрутки - дело каждого, мне не хотелось бы, чтобы мой сайт скакал влево вправо :)



Создадим обработчик события нажатия на наш триггер:

// событие нажатия на триггер открытия модального окна
modalTrigger.addEventListener("click", function () {
    // делаем модальное окно видимым
    modalBackground.style.display = "block";

    // если размер экрана больше 1366 пикселей (т.е. на мониторе может появиться ползунок)
    if (windowInnerWidth >= 1366) {
        bodyMargin();
    }

    // позиционируем наше окно по середине, где 175 - половина ширины модального окна
    modalActive.style.left = "calc(50% - " + (175 - scrollbarWidth / 2) + "px)";
});

И в завершение создадим обработчик закрытия нашего окна при нажатии на крестик или на область за модальным окном:

// нажатие на крестик закрытия модального окна
modalClose.addEventListener("click", function () {
    modalBackground.style.display = "none";
    if (windowInnerWidth >= 1366) {
        bodyMargin();
    }
});

// закрытие модального окна на зону вне окна, т.е. на фон
modalBackground.addEventListener("click", function (event) {
    if (event.target === modalBackground) {
        modalBackground.style.display = "none";
        if (windowInnerWidth >= 1366) {
            bodyMargin();
        }
    }
});

Вот и всё модальное окно. Если у Вас есть какие-нибудь замечания, похвала, критика, советы, любой фидбек - буду рад прочитать и внести корректировки! Всех обнял-приподнял :)

Комментарии (28)


  1. aleksandy
    00.00.0000 00:00
    +3

    if ((bodyElementHTML.style.overflowY = "hidden")) {
      ...
    } else {
      ...
    }

    Есть подозрение, что else-ветка тут лишняя.

    Или...

    ... условие надо на == переделать.


    1. hahavenn Автор
      00.00.0000 00:00

      Прочитал Ваш комментарий, да, функция немного была некорректно составлена с лишними телодвижениями, теперь достаточно просто сделать отступ на величину ширины ползунка прокрутки. Спасибо!


  1. aio350
    00.00.0000 00:00
    +17

    Простейшая модалка:

    <dialog style="padding: 0">
      <div id="modal-box" style="padding: 1rem">
        <div>Modal content</div>
        <button id="close-modal-btn">Close</button>
      </div>
    </dialog>
    <button id="show-modal-btn">Show modal</button>
    
    const modal = document.querySelector('dialog')
    const modalBox = document.getElementById('modal-box')
    const showModalBtn = document.getElementById('show-modal-btn')
    const closeModalBtn = document.getElementById('close-modal-btn')
    
    let isModalOpen = false
    
    showModalBtn.addEventListener('click', (e) => {
      modal.showModal()
      isModalOpen = true
      e.stopPropagation()
    })
    
    closeModalBtn.addEventListener('click', () => {
      modal.close()
      isModalOpen = false
    })
    
    document.addEventListener('click', (e) => {
      if (isModalOpen && !modalBox.contains(e.target)) {
        modal.close()
      }
    })
    


    1. LinaRSH
      00.00.0000 00:00

      Я ждал Ваш комментарий. Если бы была возможность — плюсанул бы Вам за использование нативных инструментов без костылей и велосипедов


      1. hahavenn Автор
        00.00.0000 00:00

        Тег dialog является экспериментальным. Взять тот же mdn, к примеру firefox. Версия, где поддерживается этот тег выпущена 2022-03-08. Для safari аналогичная ситуация - 2022-03-15. Поэтому на простых костылях сделал окно для возможности использования без проблем в старых браузерах. Спасибо за комментарий в любом случае!


        1. alexnozer
          00.00.0000 00:00

          Проблема с плохой поддержкой dialog решается при помощи полифила. В браузере проверяется поддержка dialog и этот скрипт подключается при помощи динамического импорта, если браузер не поддерживает dialog.

          Преимущества этого элемента в том, что он нативный, имеет встроенные методы и атрибуты для открытия/закрытия, семантику и доступен из коробки.

          Спроектировать хорошее модальное окно не такая уж и тривиальная задача. Нужно стилиовать окно и бэкдроп, при этом предусмотреть возможность кастомизации. Нужно написать логику и API для работы, при этом предусмотреть различные сценарии использования. Нужно позаботиться о семантике и сделать его действительно модальным - роль, все нужные aria-состояния и focus trap. Нужно продумать работу с клавиатуры, перемещение по интерактивным элементам внутри, закрытие через крестик и Esc. Нужно правильно реализовать бэкдроп, чтобы контент под ним не был интерактивным и страница не прокручивалась. Также нужно учесть, что в окне может быть разный контент и не допустить переполнения. И это все ещё нужно реализовать максимально кросбраузерно.


          1. hahavenn Автор
            00.00.0000 00:00

            Спасибо за развернутый и подробный комментарий! В данном примере я не старался использовать полифилы лишь по одной причине - данный тип модального окна, на мой взгляд, достаточно прост и не занимает много строчек кода, используя также самые обычные средства для реализации. Вся логика и API предоставляются для Читателя. Об этом я постарался подумать и сделал возможность позиционирования и верстки элементов внутри окна при открытии. Все остальные пожелания или изменения в работе окна, а также взаимодействие с ним - индивидуальны и зависят от задачи)


  1. scoffs
    00.00.0000 00:00
    +2

    z-index всё же следует поднять, потому что на сайте могут быть обычные элементы, у которых z-index больше одного.


    1. hahavenn Автор
      00.00.0000 00:00

      Да, Вы правы, величину надо регулировать в зависимости от необходимого наслоения. Я указал его равным 1 из соображений, что элемент со свойством z-index один на странице. Спасибо!


  1. ZetaTetra
    00.00.0000 00:00
    +3

    Такой крестик можно нарисовать используя CSS transform:

    .modalClose {
     float:right;
     cursor:pointer;
     position:relative;
     width:20px;
     height:20px;
     display:flex;
     flex-direction:column;
     justify-content:center
    }
    .modalClose::before,
    .modalClose::after {
     position:absolute;
     content:'';
     width:100%;
     height:2px;
     background-color:#000;
    }
    .modalClose::before {
     transform:rotate(45deg);
    }
    .modalClose::after {
     transform:rotate(-45deg)
    }

    В таком случае будет проще добавить, скажем, hover, а не заморачиваться со сменой цветов в SVG через filter:

    .modalClose:hover::before,
    .modalClose:hover::after {
     background-color:Red;
    }


    1. hahavenn Автор
      00.00.0000 00:00

      Спасибо за фидбек! Честно, не хотелось особо сильно заморачиваться с CSS. Поэтому и сделал расчет из удобства, чтобы вставить любую картинку, выступающую в роли "крестика"


  1. BigDflz
    00.00.0000 00:00
    +6

    хабр стал похож на отчетную страницу первого класса, огромное достижение - модальное окно....
    ну ж если и выкладывать - то уж хотя бы c с использованием template, Shadow DOM, grid, ну и возможностью перемещения этого окна по экрану.

    и за что плюсанули в карму? за приглашение от НЛО?


    1. hahavenn Автор
      00.00.0000 00:00

      Спасибо за комментарий! Первая мысль, как сделать модальное окно - с использованием самых простых вещей без особого изощрения. Если есть примеры создания модального окна с этими инструментами - я буду рад их увидеть! Честно говоря не представляю как использовать теневое дерево, разве что скрыть модальное окно, чтобы его нельзя было достаточно легко отобразить через средства разработчиков. Но опять же, не совсем знаю, как эта технология устроена, ибо не использую, а читал для ознакомления.


    1. MrRewolwer
      00.00.0000 00:00

      Зачем вы так говорите? Это же модалка на ЧИСТОМ CSS и JS. Раньше модалку можно было только на React + Tailwind, а теперь вот, прогресс!


      1. hahavenn Автор
        00.00.0000 00:00

        Спасибо за комментарий! Я рад, что у Вас получилось освоить построение веб-страниц сразу же начиная с использованием фреймворков. Простое модальное окно на ванильном JS и CSS ничем не хуже их аналогов с использованием соответствующих библиотек, тем более в обучении (как я и указал, что не так давно занялся веб-разработкой)


  1. dmitry-rakovich
    00.00.0000 00:00

    Для этого в html добавили тег dialog, со встроенными методами открытия и закрытия через js и оверлеем)


    1. hahavenn Автор
      00.00.0000 00:00

      Да, он является экспериментальным. Хоть и поддержка от chrome была ещё в 2014 году, но тот же самый safari и firefox начали поддерживать его в марте 2022 года. Поэтому я и решил сделать простейшее модальное окно с использованием "палок". В любом случае - спасибо за фидбек!


  1. web3_Venture
    00.00.0000 00:00

    Я ожидал увидеть нативный dialog , после прочтения статьи еще раз заголовок прочитал...


    1. hahavenn Автор
      00.00.0000 00:00

      Нативный dialog поддерживается не так давно в полном объеме, о чем пишет mdn. Поэтому и модальное окно сделано "нативнее" нативного :). Спасибо за фидбек!


      1. Serator
        00.00.0000 00:00

        А модальность окна в чем заключается? Статья про div в виде окна, не более. Нативный dialog - не просто тег и он, как минимум, уже поддерживается. Почитайте еще про атрибут inert, технику focus trap, accessibility... И обновите свой пример, он сломан, так как modalTrigger пропущен.


        1. hahavenn Автор
          00.00.0000 00:00

          Спасибо за комментарий! Модальность заключается в том, что при нажатии на modalTrigger у нас открывается окно поверх других элементов на странице. Сам же modalTrigger я отдельно вынес и указал, что его задаем сами (т.е. любой элемент на Вашей странице). Спасибо также за материал, прочитаю и ознакомлюсь!


          1. Serator
            00.00.0000 00:00

            Модальность должна ограничивать пользователя во взаимодействии с родительским окном. В вашем случае доступ с клавиатуры никак не блокируется. Только указатель (к примеру, курсор мыши) и то при условии, что всякие там z-index у родителя и его потомков не будут перекрывать ваше окно. Проблема всей статьи в том, что слой поверх слоя можно было и 10 лет назад сделать относительно просто на "чистом CSS и JS", а вот именно модальное окно уже сильно сложней, если вообще осуществимо даже сейчас (т.е. воссоздать поведение нативного dialog в полной мере).


  1. savostin
    00.00.0000 00:00
    +2

    Да зачем там вообще Javascript. Выбирайте.


    1. hahavenn Автор
      00.00.0000 00:00

      JS там за тем, что статья называется "Модальное окно на чистом CSS и JS" :)

      В любом случае - спасибо за дополнительные вариации модальных окон, там есть реально классные варианты!


  1. Horston
    00.00.0000 00:00

    Мне как новенькому в веб разработке, было очень полезно прочитать вашу статью


    1. hahavenn Автор
      00.00.0000 00:00

      Спасибо! Сейчас думаю, какой материал выкладывать периодически, по мере сложности и полезности. Планирую как и по фронтенду, так и по бекенду


  1. hitriymuh
    00.00.0000 00:00
    +1

    .modalActive {
        position: absolute;
        width: 350px;
        height: 495px;
        top: 50%;
        left: 50%;
        border-radius: 10px;
        background-color: rgb(255, 255, 255);
        cursor: default;
        padding: 40px 20px;
        transform: translate(-50%, -50%)
    }

    Если в дальнейшем потребуется изменение размеров модального окна у вас возникнет потребность с пересчетом top и left. Используйте для центрирование transform. Почитать подробнее про него можно здесь.


    1. hahavenn Автор
      00.00.0000 00:00

      Спасибо! Да, такой способ центрирования намного удобнее и не зависит от параметров окна. Применил у себя :)

      upd: при наличии ползунка прокрутки модальное окно не учитывает его величину и надо задавать дополнительный отступ, чтобы окно позиционировалось по центру. Этот способ удобен, когда мы не учитываем длину страницы по оси Y и нам неважно, будет ли сайт смещаться влево или нет. В моём случае я дополнительно смещаю сайт, чтобы он всегда был по центру, поэтому надо продумать момент с позиционированием окна тоже