Реализация всплывающих окон (Modal/Popup) на сайте

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация всплывающих окон (Modal/Popup) на сайте
Простая
~1 рабочий день
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Реализация всплывающих окон (Modal/Popup) на сайте

Модальные окна — один из компонентов с наибольшим количеством скрытых проблем: управление фокусом, скролл под модалкой, z-index конфликты, анимации, мобильный клавиатурный сдвиг. Нативный <dialog> решает большую часть без JS.

Нативный <dialog>: правильная основа

<dialog id="confirm-modal" class="modal">
  <div class="modal__content">
    <button class="modal__close" autofocus aria-label="Закрыть">✕</button>
    <h2 class="modal__title">Подтверждение действия</h2>
    <p>Вы уверены, что хотите удалить запись?</p>
    <div class="modal__actions">
      <button class="btn btn--danger" id="confirm-btn">Удалить</button>
      <button class="btn" id="cancel-btn">Отмена</button>
    </div>
  </div>
</dialog>
const modal = document.getElementById('confirm-modal') as HTMLDialogElement

// showModal() vs show(): showModal блокирует фон через ::backdrop
function openModal() {
  modal.showModal()
  document.body.style.overflow = 'hidden'  // блокируем скролл фона
}

function closeModal() {
  modal.close()
  document.body.style.overflow = ''
}

// Закрытие по Escape встроено в <dialog>
// Закрытие по клику на backdrop
modal.addEventListener('click', (e) => {
  const rect = modal.getBoundingClientRect()
  const clickedBackdrop = (
    e.clientX < rect.left || e.clientX > rect.right ||
    e.clientY < rect.top  || e.clientY > rect.bottom
  )
  if (clickedBackdrop) closeModal()
})

document.getElementById('cancel-btn')!.addEventListener('click', closeModal)
document.querySelector('.modal__close')!.addEventListener('click', closeModal)
dialog.modal {
  padding: 0;
  border: none;
  border-radius: 12px;
  max-width: 500px;
  width: calc(100% - 32px);
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
  animation: modal-in 0.2s ease;
}

dialog.modal::backdrop {
  background: rgba(0, 0, 0, 0.5);
  backdrop-filter: blur(2px);
  animation: backdrop-in 0.2s ease;
}

@keyframes modal-in {
  from { opacity: 0; transform: scale(0.95) translateY(-10px); }
  to   { opacity: 1; transform: scale(1) translateY(0); }
}

@keyframes backdrop-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Анимация закрытия — через класс */
dialog.modal.closing {
  animation: modal-out 0.15s ease forwards;
}

@keyframes modal-out {
  to { opacity: 0; transform: scale(0.95); }
}

React: universal Modal компонент

import { useEffect, useRef, ReactNode } from 'react'
import { createPortal } from 'react-dom'

interface ModalProps {
  isOpen: boolean
  onClose: () => void
  title?: string
  children: ReactNode
  size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'
  closeOnBackdrop?: boolean
}

export function Modal({
  isOpen, onClose, title, children,
  size = 'md',
  closeOnBackdrop = true,
}: ModalProps) {
  const dialogRef = useRef<HTMLDialogElement>(null)
  const previousFocusRef = useRef<HTMLElement | null>(null)

  useEffect(() => {
    const dialog = dialogRef.current
    if (!dialog) return

    if (isOpen) {
      previousFocusRef.current = document.activeElement as HTMLElement
      dialog.showModal()
      document.body.style.overflow = 'hidden'
    } else {
      dialog.close()
      document.body.style.overflow = ''
      previousFocusRef.current?.focus()
    }
  }, [isOpen])

  // Синхронизируем с нативным close (Escape)
  useEffect(() => {
    const dialog = dialogRef.current
    const handleClose = () => onClose()
    dialog?.addEventListener('close', handleClose)
    return () => dialog?.removeEventListener('close', handleClose)
  }, [onClose])

  function handleBackdropClick(e: React.MouseEvent<HTMLDialogElement>) {
    if (!closeOnBackdrop) return
    const rect = dialogRef.current!.getBoundingClientRect()
    if (
      e.clientX < rect.left || e.clientX > rect.right ||
      e.clientY < rect.top  || e.clientY > rect.bottom
    ) {
      onClose()
    }
  }

  return createPortal(
    <dialog
      ref={dialogRef}
      className={`modal modal--${size}`}
      onClick={handleBackdropClick}
      aria-labelledby={title ? 'modal-title' : undefined}
    >
      <div className="modal__content" onClick={e => e.stopPropagation()}>
        {title && (
          <div className="modal__header">
            <h2 id="modal-title" className="modal__title">{title}</h2>
            <button className="modal__close" onClick={onClose} aria-label="Закрыть">
              <svg viewBox="0 0 24 24" width="20" height="20">
                <path d="M6 6l12 12M18 6l-12 12" stroke="currentColor" strokeWidth="2"/>
              </svg>
            </button>
          </div>
        )}
        <div className="modal__body">{children}</div>
      </div>
    </dialog>,
    document.body
  )
}

Focus trap: удержание фокуса внутри

Нативный <dialog> с showModal() уже обеспечивает focus trap. Для кастомных решений:

function trapFocus(element: HTMLElement): () => void {
  const focusable = element.querySelectorAll<HTMLElement>(
    'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])'
  )
  const first = focusable[0]
  const last = focusable[focusable.length - 1]

  function handleTab(e: KeyboardEvent) {
    if (e.key !== 'Tab') return
    if (e.shiftKey) {
      if (document.activeElement === first) {
        e.preventDefault()
        last.focus()
      }
    } else {
      if (document.activeElement === last) {
        e.preventDefault()
        first.focus()
      }
    }
  }

  element.addEventListener('keydown', handleTab)
  first?.focus()

  return () => element.removeEventListener('keydown', handleTab)
}

Стек модалок (вложенные)

class ModalStack {
  private stack: HTMLDialogElement[] = []

  open(dialog: HTMLDialogElement) {
    this.stack.push(dialog)
    dialog.showModal()
    document.body.style.overflow = 'hidden'
  }

  close() {
    const top = this.stack.pop()
    top?.close()
    if (this.stack.length === 0) {
      document.body.style.overflow = ''
    }
  }

  closeAll() {
    while (this.stack.length) {
      this.stack.pop()?.close()
    }
    document.body.style.overflow = ''
  }
}

export const modalStack = new ModalStack()

Bottomsheet для мобильных

На мобильных модалка снизу — нативнее, чем по центру экрана:

@media (max-width: 768px) {
  dialog.modal {
    margin: auto auto 0;
    width: 100%;
    max-width: 100%;
    border-radius: 16px 16px 0 0;
    max-height: 90dvh;
    animation: sheet-in 0.3s cubic-bezier(0.32, 0.72, 0, 1);
  }

  dialog.modal::backdrop {
    align-items: flex-end;
  }

  @keyframes sheet-in {
    from { transform: translateY(100%); }
    to   { transform: translateY(0); }
  }
}

Скролл внутри модалки на iOS

На iOS -webkit-overflow-scrolling: touch внутри position: fixed элементов работает нестабильно:

.modal__body {
  overflow-y: auto;
  max-height: calc(90dvh - 80px);  /* dvh вместо vh — учитывает мобильный UI */
  overscroll-behavior: contain;    /* не передавать скролл на фон */
  -webkit-overflow-scrolling: touch;
}

Confirm-диалог вместо нативного window.confirm

// Использование:
// const confirmed = await confirm({ title: 'Удалить?', message: 'Это действие нельзя отменить' })

let resolveConfirm: (value: boolean) => void

export function confirm(options: { title: string; message: string }): Promise<boolean> {
  return new Promise(resolve => {
    resolveConfirm = resolve
    // Открываем компонент ConfirmModal через глобальный стейт или event bus
    eventBus.emit('confirm:open', options)
  })
}

function ConfirmModal() {
  const [state, setState] = useState<{ title: string; message: string } | null>(null)

  useEffect(() => {
    eventBus.on('confirm:open', setState)
    return () => eventBus.off('confirm:open', setState)
  }, [])

  const close = (result: boolean) => {
    resolveConfirm?.(result)
    setState(null)
  }

  if (!state) return null
  return (
    <Modal isOpen title={state.title} onClose={() => close(false)} closeOnBackdrop={false}>
      <p>{state.message}</p>
      <div className="modal__actions">
        <button onClick={() => close(true)}>Подтвердить</button>
        <button onClick={() => close(false)}>Отмена</button>
      </div>
    </Modal>
  )
}

Сроки

Нативный <dialog> с базовой анимацией и закрытием — 3–4 часа. React-компонент с порталом, focus trap, bottomsheet для мобильных — 1 день. Система с confirm/alert API, стеком модалок и доступностью по WCAG — 1.5–2 дня.