Как сделать кнопку закрытия окна в html
Обновлено: 03.05.2024
Уверен, многие хоть раз создавали всплывающее модальное окно. Но задумывались ли вы об определении этого компонента? Как он должен работать?
В этом материале я постарался собрать максимально полный свод правил, рекомендаций и примеров реализации по которым модальные окна должны работать.
Я покажу, как просто создавать сложные, удобные, производительные и доступные модальные окна независимо от браузера, платформы, устройства или способа взаимодействия пользователя.
Этот список сформирован на основе спецификаций WAI-ARIA, HTML Living Standard и моего личного опыта. И хотя я буду говорить про веб, большинство правил и рекомендаций применимы для модальных окон где угодно.
Определение модального окна
Модальное окно — это окно наложенное либо на документ, либо на другие окна. При этом, любой контент под модальным окном является недоступным для взаимодействия.
Теги и атрибуты
Простейшая реализация кнопки открывающая диалог по его id:
Для различных диалогов, уведомлений и прочих перекрывающих документ элементов существует тег . Его вы и должны использовать. К огромному сожалению, его поддержка не самая лучшая:
- Chromium — полная поддержка.
- Firefox — поддержка за флагом.
- Safari не поддерживает вовсе.
Так что для этих браузеров нужно подгружать polyfill:
Вы, конечно, можете использовать и другой элемент для реализации диалогового окна, например так:
но тогда вам придётся самостоятельно реализовывать всё поведение описанное далее. В то время как с большую часть браузер реализует из коробки.
Внешний вид и содержание
Вскользь коснусь внешнего вида.
На небольших экранах диалоговое окно должно занимать 100% его размера. Если ваш диалог будет большим:
- Его будет легче "нащупать". Дело в том, что пользователь может взаимодействовать со страницей следующим образом: он водит пальцем по дисплею, а программа чтения с экрана озвучивает то, что в данный момент находится под пальцем.
- Пользователю гарантированно не будут озвучиваться элементы "под ним". Иначе, например, VoiceOver на iPad может озвучивать отдельные фрагменты страницы под модальным окном даже "сквозь" оверлей блокирующий доступ указателю.
- Вы скроете прокрутку фона на некоторых устройствах при прокрутке контента в диалоговом окне.
- Удобнее для одной руки. Если окно растянуто на всю высоту – то у вас есть возможность прижать кнопки управления к нижней части дисплея. Туда намного проще дотянуться одной рукой пользователям современных смартфонов.
- Больше места для контента на устройствах с маленьким экраном, таких как iPhone SE.
Заголовок обязателен
У модального окна, как у любой обычной страницы, должен быть свой заголовок. Короткий, точно описывающий его предназначение. Наличие заголовка намного упрощает восприятие пользователем.
Настоятельно рекомендуется использовать для заголовка тег - .
Но просто добавить заголовок в диалоговое окно недостаточно. Их нужно ещё и логически "связать". Сделать это можно с помощью атрибута aria-labelledby следующим образом:
Теперь, при попадании пользователя в диалоговое окно, в случае с экранным диктором, будет зачитан не только факт наличия диалога, но и его заголовок.
Статический контент должен быть связан с окном
Если в вашем диалоговом окне есть какое-то не интерактивное содержание, например, абзац текста, его стоит связать с диалогом подобно заголовку. Иначе, в некоторых случаях программы чтения с экрана не будут озвучивать такой контент.
Делается это атрибутом aria-describedby :
Если в вашем диалоговом окне много контента, тогда стоит обернуть его в один и связать элемент диалога уже с ним:
Важно! Заголовок и любые кнопки не относящиеся к содержимому, а служащие для управления диалоговым окном, не должны быть включены в элемент на который указывает aria-describedby . Они должны быть вынесены отдельно:
Интерактивные элементы связывать не нужно
Есть другой сценарий, когда содержимое вашего окна состоит из формы без предшествующего ей текста. В таком случае нет необходимости связывать форму с окном:
Элементы формы являются интерактивными. И они будут озвучены скринридером, когда пользователь начнёт с ними взаимодействовать.
Если скомбинировать и статический текст и форму:
Способы закрыть окно
Дополнительно, в зависимости от вашей логики, вы можете позволить пользователю закрыть диалог кликнув за его пределами или нажав Escape (встроено в из коробки).
- Не рассчитывайте, что пользовать всегда может нажать на оверлей и так закрыть диалог.
- Как я писал ранее, во многих случаях диалоговое окно может занимать всю или большую часть экрана. Таким образом попасть в него может быть сложно или невозможно.
- Такой оверлей семантически не считается интерактивным элементом. Он не может быть в фокусе и на него невозможно "нажать" клавишами.
Простейшая реализация кнопки закрывающей родительский диалог:
А если вы делаете кнопку с иконкой, то не забывайте про подпись, чтобы передать ёё назначение:
Поведение фокуса
При открытии диалога
Во время открытия диалогового окна фокус должен быть перемещён на элемент внутри него. На какой именно — зависит от содержания.
В общем случае фокус перемещается на первый интерактивный элемент. Именно так ведет себя нативный в браузере. Но нельзя делать сам элемент окна фокусируемым и перемещать фокус на него.
Например, для диалога с формой первый интерактивный элемент это первый . Если ваше диалоговое окно носит чисто информативный характер, например, уведомление об успешной подписке, тогда первым и единственным элементом будет кнопка закрывающая диалог.
Но есть и несколько исключений:
- Запрос подтверждения чего-либо. Если ваш диалог запрашивает у пользователя подтверждения перед выполнением каких-то необратимых действий (удаление чего-то или выполнение финансовых операций), тогда фокус автоматически должен ставится на кнопку "отмены" этих действий, независимо от её расположения.
- Ситуации, когда в диалоговом окне много статического контента и первый интерактивный элемент не помещается в видимую область. Проблема тут в том, что в таком случае браузер автоматически проскролит вниз к кнопке в фокусе. Это вынудит пользователя скролить обратно вверх, а потом снова вниз. Для таких случаев есть два подхода:
- Переместить или продублировать интерактивные элементы так, чтобы первый из них был в видимой части экрана. Например, выполнить кнопку закрыть в виде крестика и закрепить в верхней части диалогового окна.
- Заголовок или первый абзац текста нужно сделать фокусируемым при помощи tabindex="-1" и перемещать фокус на него. Но при этом подходе некоторые программы чтения с экрана могут озвучивать заданный текст дважды: сначала как заголовок и описание окна, а потом как содержание выделенного элемента.
Управлять куда именно попадёт фокус при открытии модального окна можно с помощью атрибута autofocus :
Внутри диалога
Особенность модального окна в том, что оно перекрывает собой весь документ не давая возможность с ним взаимодействовать.
Чтобы блокировать указатель обычно документ накрывается полупрозрачным блоком.
Но этого недостаточно, так как остаётся ещё и навигация клавишами Tab / Shift + Tab . Также это могут быть клавиши громкости на смартфонах или специальные клавиши на дополнительных инструментах подключенных по USB/Bluetooth. Этот способ навигации тоже должен быть заблокирован.
После попадания фокуса в модальное окно пользователь может перебирать интерактивные элементы внутри этого окна, но не должен выходить за его пределы. Другими словами, такое диалоговое окно работает как ловушка для фокуса. Это поведение встроено в , так что от вас никаких действий не требуется. А вот используя другой элемент с role="dialog" его нужно реализовывать самостоятельно средствами JavaScript.
При закрытии диалога
При закрытии диалогового окна фокус должен быть перемещён туда, где он был в момент открытия. Это поведение не является частью и браузер полностью оставляет это на усмотрение разработчика.
Но и тут есть одно исключение: если элемент более не доступен, тогда фокус нужно вернуть туда, откуда наиболее логично для пользователя продолжить работу.
Пример
Предлагаю разобрать на примере. Представим систему из трех диалоговых окон:
- Сообщает пользователю об наличии подписки. В нем две кнопки: "Условия подписки" и "Подписаться"
- Отображается по клику на "Условия подписки". Открывается поверх первого.
- Отображается по клику на "Подписаться". Заменяет собой первое.
В примерах ниже я специально пропустил дополнительные атрибуты и элементы, для упрощения кода.
Итак, у нас есть стартовая кнопка.
По нажатию на неё открывается первый диалог. Фокус автоматически перемещается на первый интерактивный элемент. А закрытие диалога должно возвращать фокус назад.
Далее пользователь перемещает фокус на "Условия подписки" и нажимает. Открывается второй диалог поверх первого. Фокус перемещается в него, а возвращаться должен на эту же кнопку в первом диалоге:
После закрытия второго диалога ваш JavaScript должен вернуть фокус на кнопку "Условия подписки" в первом.
После чего пользователь нажимает кнопку "Подписаться". По условиям нашей задачи открывается третий диалог. Фокус автоматически перемещается в него. А первый диалог закрывается:
И вот проблема: третье окно должно вернуть фокус на кнопку в первом, но первое окно больше не доступно. В таких случаях фокус нужно вернуть туда, куда указывал закрытый диалог — на кнопку "Рассылка" с которой пользовать начал.
Безусловно, в вашем конкретном случае может быть более логичное поведение для возвращения фокуса. Например, у вас диалог создания новой записи в таблице. В таком случае, может быть логичнее возвращать фокус на только что созданную запиcь.
Помните, как во время установки программы в Windows можно просто нажимать Enter? Так вот это пример хорошей работы с фокусом: каждый раз, при переходе на новый экран в фокус ставится элемент, с которым вы скорее всего будете взаимодействовать — кнопка "Далее" или "Обзор".
На модальных окнах, на рекламных объявлениях, на других подобных всплывающих элементах веб-страниц часто можно найти кнопку «Закрыть» с соответствующим символом. Эти кнопки позволяют пользователям (по крайней мере — некоторым из них) закрывать окна. Возможность щёлкнуть по кнопке «Закрыть» часто доступна только тем посетителям веб-страниц, у которых есть мышь. Дело в том, что большинство реализаций подобных кнопок далеко не идеально. В материале, перевод которого мы сегодня публикуем, будет рассмотрено 11 проблемных паттернов, которые используются при создании кнопок «Закрыть», а также — способы решения проблем этих паттернов. Здесь же речь пойдёт и об удачных способах создания кнопок «Закрыть».
Неудачные паттерны
▍Паттерн №1: элемент div и фоновое изображение
Взглянем на HTML-код, используемый для создания кнопки «Закрыть»:
Проблемы и их решение
Ниже приведён список проблем, которые характерны для данного варианта реализации кнопки «Закрыть». Зная об этих проблемах, можно наметить пути их решения.
- Элемент — это элемент, которым пользуются в крайнем случае, тогда, когда для решения некоей задачи просто нет другого, более подходящего элемента. Использование вместо более подходящих элементов ведёт к плохой доступности проекта.
- Событие click элемента вызывается только в том случае, если по нему щёлкают мышью. А, например, такое же событие элемента вызывается и по щелчку мыши, и по нажатию клавиш Enter и Пробел на клавиатуре.
- Элемент не поддерживает получение фокуса с клавиатуры.
- Здесь нет текстовой альтернативы фоновому изображению.
- Средства для чтения с экрана не «озвучивают» этот элемент.
▍Паттерн №2: элемент div и иконка
Проблемы и их решение
- Значок ✕ не является чем-то таким, что можно напрямую соотнести с командой «Закрыть». Этот значок используется для записи математических выражений, в которых одно число умножается на другое (вроде 2✕2). Не стоит использовать его для оформления кнопки «Закрыть».
- О минусах использования элемента при оформлении кнопок «Закрыть» читайте в разделе «Паттерн №1».
- Средства для чтения с экрана «озвучат» этот значок, использовав какое-нибудь слово, имеющее отношение к умножению (вроде «multiplication x» или «times»).
▍Паттерн №3: иконки из Font Awesome
Проблемы и их решение
▍Паттерн №4: ссылка «Закрыть»
Проблемы и их решение
- Если у элемента есть атрибут href , он представляет собой ссылку на некий ресурс — вроде другой веб-страницы или PDF-документа.
- Цель элемента из этого примера заключается в вызове на той же самой странице действия, выполняемого средствами JavaScript. В такой ситуации лучше подойдёт элемент , атрибут type которого установлен в значение button . Дело в том, что такой элемент, по умолчанию, не выполняет никаких действий. Он создан специально для того, чтобы вызывать выполнение каких-то действий в ответ на события пользовательского ввода.
- Если вы не уверены в том, какой именно элемент использовать, или , посмотрите это видео.
- Средства для чтения с экрана могут «прочитать» то, что будет сгенерировано с помощью CSS. Значок, используемый в этом примере, как и в предыдущих примерах, это не значок «закрыть», а значок, символизирующий умножение. Его не стоит использовать для оформления кнопок «Закрыть».
- Средства для чтения с экрана, разбирая этот код, могут сообщить о значке умножения и о ссылке, но не о кнопке «Закрыть».
▍Паттерн №5: ссылка «Закрыть» с текстом
Вот HTML-код этого паттерна:
Проблемы и их решение
- Это, на самом деле, не так уж и плохо, но тут, всё же используется ссылка, а не кнопка.
- Почитайте то, о чём говорится в разделе «Паттерн №4» относительно элементов и содержимого, генерируемого средствами CSS.
- Средства для чтения с экрана могут «озвучить» подобный элемент как «link, times close».
▍Паттерн №6: ссылка «Закрыть» без атрибута href
Проблемы и их решение
- Перед нами ещё один пример, который нельзя признать очень уж плохим, но тут у ссылки нет атрибута href , и это, опять же, ссылка, а не кнопка.
- Если у элемента нет атрибута href , это значит, что элемент представляет собой местозаполнитель элемента, куда могла бы быть вставлена реальная ссылка.
- Если вы обрабатываете событие щелчка на ссылке-местозаполнителе, то вам, возможно, стоит использовать не такую ссылку, а ссылку с атрибутом href , или элемент . Это зависит от того, что именно происходит при щелчке по подобному элементу.
- Ссылки-местозаполнители не могут получать фокус.
- Если вы не знаете точно о том, что именно вам нужно, элемент или , обратитесь к этому видео.
- Средства для чтения с экрана могут «прочитать» этот элемент как «times, clickable».
▍Паттерн №7: ссылка-местозаполнитель и элемент img
Вот HTML-код этого паттерна:
Проблемы и их решение
- Тут нет текстовой альтернативы изображению.
- Об особенностях ссылок-местозаполнителей читайте в разделе «Паттерн №6».
- Средства для чтения с экрана могут «прочитать» в такой ситуации имя файла, например, выдать что-то вроде «close.jpg, image».
▍Паттерн №8: радиокнопка
Вот применяемый здесь стиль:Проблемы и их решение
- Когда люди, которые продвигают идеи доступности контента, говорят о том, что нужно просто использовать кнопки, они имеют в виду элемент , а не радиокнопки.
- Радиокнопки используются в группах, описывающих наборы взаимосвязанных опций.
- У SVG-изображения нет текстовой альтернативы. Вот хорошая статья о доступности SVG-изображений.
- Средства для чтения с экрана могут никак не «озвучить» такой элемент.
▍Паттерн №9: кнопка с иконкой
Как обычно, сначала рассмотрим разметку:
Проблемы и их решение
- Значок ✕, с которым мы уже встречались, это не символ закрытия чего-либо. Это знак, символизирующий умножение. Не стоит использовать его для оформления кнопки «Закрыть».
- Средства для чтения с экрана могут «прочитать» эту кнопку как «times, button».
▍Паттерн №10: кнопка с SVG-изображением
Проблемы и их решение
- Здесь нет текстовой альтернативы SVG-изображению. О доступности таких изображений читайте здесь.
- Средства для чтения с экрана могут сообщить о том, что тут имеется «button», не «сказав» больше ничего.
▍Паттерн №11: старый добрый символ X
Вот HTML-код, реализующий этот паттерн:
Проблемы и их решение
- Учитывая то, что существует элемент , в явной настройке семантики кнопки с использованием атрибута role необходимости нет.
- Если пользоваться элементом , не нужно применять атрибут tabindex . HTML-кнопки, по умолчанию, рассчитаны на возможность получения ими фокуса ввода.
- О минусах читайте в разделе «Паттерн №1».
- Символ, представляющий букву X, это не иконка для закрытия чего-либо.
- Средства для чтения с экрана могут «озвучить» этот элемент как «X».
Вот CodePen-проект, в котором собраны неудачные паттерны создания кнопок «Закрыть»
Альтернативы неудачным паттернам
▍Решение №1: кнопка с видимым текстом без иконки
Вот разметка, применяема в этом решении:
Особенности
- Здесь используется только текст. Такую кнопку просто реализовать, её назначение понятно пользователям.
- Средства для чтения с экрана могут «озвучить» такую кнопку как «Close, button».
▍Решение №2: кнопка с видимым текстом и с иконкой, скрытой от ассистивных технологий
Особенности
- Если вам и правда нужно пользоваться иконкой, которая, в обычных условиях, используется как символ умножения, её стоит скрыть от средств для чтения с экрана, поместив в элемент с атрибутом aria-hidden=«true» .
- Средства для чтения с экрана могут сообщить о том, что это «Close, button».
▍Решение №3: кнопка со скрытым текстом и со значком, который видим на экране, но скрыт от ассистивных технологий
Вот HTML-код этого решения:
Особенности
- К сожалению, нет стандартного способа скрытия элементов, при использовании которого они не видны, но остаются доступными для ассистивных технологий. Эта задача решается здесь с помощью стиля класса .sr-only .
- Средства для чтения с экрана могут «озвучить» эту кнопку как «Close, button».
▍Решение №4: ещё один вариант кнопки со скрытым текстом и со значком, который видим на экране, но скрыт от ассистивных технологий
Особенности
- Если вам не хочется выводить текст — назначьте иконке или SVG-изображению текстовую альтернативу, воспользовавшись атрибутом кнопки aria-label .
- Средства для чтения с экрана могут сообщить о том, что перед нами «Close, button».
▍Решение №5: использование Font Awesome
Вот, ради полноты изложения, удачный пример кнопки «Закрыть», при создании которой используется Font Awesome:
▍Общие замечания
Иногда имеет смысл использовать метки с более подробными описаниями действий, вроде «Закрыть окно», или «Закрыть галерею», или «Закрыть рекламу».
Если вы применяете решения сторонних разработчиков для оформления модальных окон, диалоговых окон и прочего подобного, пожалуйста, перед включением их в состав своего сайта, поинтересуйтесь тем, как именно реализованы эти решения. Не стоит слепо доверять другим в вопросах качества кода и обеспечения доступности контента.
Здесь можно найти примеры реализации удачных паттернов кнопок «Закрыть».
Как сделаны кнопки «Закрыть», которые используются в ваших проектах?
Вёрстка таких окон сначала кажется простой задачей. Модальные окна можно сделать даже без помощи JS только лишь с помощью CSS, но на практике они оказываются неудобными, и из-за маленьких недочетов модальные окна раздражают посетителей сайта.
В итоге было задумано сделать собственное простое решение.
Вообще говоря, есть несколько готовых скриптов, JavaScript библиотек, реализующих функционал модальных окон, например:
- Arctic Modal,
- jquery-modal,
- iziModal,
- Micromodal.js,
- tingle.js,
- Bootstrap Modal (из библиотеки Bootstrap) и др.
(в статье не рассматриваем решения на базе Frontend-фреймворков)
Несколькими из них я пользовался сам, но почти у всех находил какие-то недостатки. Некоторые из них требуют подключения библиотеки jQuery, которая есть не на всех проектах. Для разработки своего решения, нужно сначала определиться с требованиями.
Что мы ждём от модальных окон? Отвечая на этот вопрос, я основывался на докладе «Знакомьтесь, модальное окно» Анны Селезнёвой, а так-же на относительно старой статье NikoX «arcticModal — jQuery-плагин для модальных окон».
Итак, чтобы нам хотелось видеть?
Дисклеймер: Прежде чем мы рассмотрим подробности, сразу дам ссылку на готовый код получившейся библиотеки (HystModal) на GitHub, а также ссылку на демо+документацию.
Начнём с разметки.
1. Разметка HTML и CSS
1.1. Каркас модальных окон
Как открыть окно быстро? Самое простое решение: разместить всю разметку модального окна сразу в HTML странице. Затем скрывать/показывать это окно при помощи переключения классов CSS.
Набросаем такую разметку HTML (я назвал этот скрипт «hystmodal»):
Сделаем так, чтобы .hystmodal растягивался на всё окно браузера и закрывал собой содержимое страницы. Чтобы этого добиться, установим фиксированное позиционирование в CSS и приравняем свойства top, bottom, left и right к нулю.
В этом коде сделаны ещё две вещи:
- Так как мы хотим центрировать окно внутри страницы, превращаем .hystmodal в flex-контейнер с выравниваем его потомков по центру по вертикали и горизонтали.
- Окно может быть больше высоты экрана браузера, поэтому мы устанавливаем overflow-y: auto , чтобы при переполнении возникала полоса прокрутки. Также, для сенсорных экранов (в основном для Safari) нам стоит установить свойство -webkit-overflow-scrolling: touch , чтобы сенсорная прокрутка работала именно на этом блоке а не на странице.
Теперь установим стили для самого окна.
Кажется возникли сложности.
Проблема №1. Если высота окна больше высоты окна браузера, то контент окна будет обрезан сверху.
Это возникает из-за свойства justify-content: center . Оно позволяет нам удобно выровнять потомков по основной оси (по вертикали), но если потомок становится больше родителя то часть его становится недоступной даже при прокручиваемом контейнере. Подробнее можно посмотреть на stackoverflow. Решение – установить justify-content: flex-start , а потомку установить margin:auto . Это выровняет его по центру.
Проблема №2. В ie-11 если высота окна больше высоты окна браузера, то фон окна обрезается.
Решение: мы можем установить flex-shrink:0 потомку – тогда обрезки не происходит.
Проблема №3. В браузерах кроме Chrome нет отступа от нижней границы окна (т.е. padding-bottom не сработал).
Сложно сказать баг это браузеров или наоборот соответствует спецификации, но решения два:
- установить псевдоэлемент ::after после потомка и дать ему высоту вместо padding
- обернуть элемент в дополнительный блок и дать отступы уже ему.
Воспользуемся вторым методом. Добавим обертку .hystmodal__wrap . Так мы заодно обойдём и проблему №1, а вместо padding у родителя установим margin-top и margin-top у самого .hystmodal__window .
Наш итоговый html:
В код также добавлены некоторые aria и role атрибуты для обеспечения доступности.
Обновленный код CSS для обертки и окна.
1.2 Скрываем окно
Сейчас наше окно всегда видно. Когда говорят о скрытии элементов, первое что приходит на ум это переключать свойство display со значения none до нашего значения flex.
Но этот подход нас не устроит, ведь свойство display не анимируется. Все переходы дочерних элементов, указанные в свойстве transition, работать не будут.
Нам поможет другое свойство visibility:hidden . Оно скроет окно визуально, хотя и зарезервирует под него место. А так как все будущие окна на странице имеют фиксированное
позиционирование – они будут полностью скрыты и не повлияют на остальную страницу. Кроме того, на элементы с visibility:hidden нельзя установить фокус с клавиатуры, а от скрин-ридеров мы уже скрыли окна с помощью атрибута aria-hidden="true" .Добавим также классы для открытого окна:
1.3 Оформление подложки
В лучшем случае, нам нужен отдельный html-элемент в качестве оверлея. Можно использовать и имеющийся элемент модального окна .hystmodal в качестве оверлея, но тогда анимация на этом элементе (например переход opacity) будет затрагивать и внутренние элементы. В итоге, не получится анимировать разные свойства для окна и оверлея отдельно.
Просто разместим элемент .hystmodal__shadow прямо перед закрывающим