Свободно стилизируемы outline DOM элементов.

 Публичный пост
13 января 2021  387

В последнее время всё чаще и чаще возникает вопрос о доступности, если раньше скрытие outline для элементов страницы было общим правилом, то теперь хороший сайт должен иметь outline у элементов, как минимум :focus-visible.
Основная проблема outline - это их стилизирование.

Я пришел к своему решению, которое изложено в этой статье.
Gif весрия

Я встретил интересный случай, работая над своим pet проектом.
Мне нужно было сделать один и тот же стиль обводки (при наведении и фокусе) для элементов визуализаций и всех фокусируемых элементов DOM.

Мое решение

Вставляем div поверх всего остального контента в document.body , и отключаем ему обработку событий через pointer-events: none, растягиваем в размер документа, z-index должен быть больше всех остальных на странице.

.overlay {
  pointer-events: none;
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 1000;
  transition: opacity 0.1s;
}

Добавляем еще 4 div с абсолютными позициями в ранее добавленный родительский:

<div class="overlay" id="overlay">
  <div class="border border--leftTop" id="leftTop"></div>
  <div class="border border--leftBottom" id="leftBottom"></div>
  <div class="border border--rightTop" id="rightTop"></div>
  <div class="border border--rightBottom" id="rightBottom"></div>
</div>

их стили (scss):

.border {
  position: absolute;
  top: -4px;
  left: -4px;
  transform: translate(-20px, -20px);
  transition: transform;
  transition-duration: 0.1s;
  border-style: solid;	
  border-width: 1px;
  border-color: red;
  width: 8px;	
  height: 8px;	

  &--leftTop {
    transform: translate3d(-20px, -20px, 1px);	
    border-bottom-color: transparent;
    border-right-color: transparent;
  }	
  &--leftBottom {
    transform: translate3d(-20px, calc(100vh + 20px), 1px);	
    border-top-color: transparent;	
    border-right-color: transparent;
  }	
  &--rightTop {	
    transform: translate3d(calc(100vw + 20px), -20px, 1px);	
    border-bottom-color: transparent;
    border-left-color: transparent;	
  }	
  &--rightBottom {
    transform: translate3d(calc(100vw + 20px), calc(100vh + 20px), 1px);
    border-top-color: transparent;	
    border-left-color: transparent;	
  }	
} 

Добавляем подписку на события для document: pointerenter, pointerleave , focus , blur:

document.addEventListener('focus', onFocus, true);
document.addEventListener('blur', onBlur, true);
document.addEventListener('pointerenter', onFocus, true);
document.addEventListener('pointerleave', onBlur, true);

Не забудьте отписаться если вы делаете какое то React приложение, и вюшка может быть отмонтирована.


В функциях слушателей фильтруем все события по tabIndex > -1 у event.target. При этом также проверяем что у ссылок есть href, и кроме того что у элементов нет атрибута disabled. Когда происходит blur может оказаться, что элемент оказался в контейнере, который тоже может иметь фокус (тут конечно можно задаться вопросом семантики, но такое бывает... почему в <button> находится <a> 😂):

const checkHref = (element) => {
  const tag = element.tagName;
  return !(tag === 'A' || tag === 'AREA') || element.href;
};
const onFocus = (event) => {
  if (
    event?.target?.tabIndex > -1 
    && current !== event.target
  ) {
    // skip anchors and areas without href and disabled items
    if (!checkHref(event.target) || event.target.disabled) {
      return;
    }
    current = event.target;
    show(current);
  }
};
const onBlur = (event) => {
  if (event?.target?.tabIndex > -1) {
    // find parent item that can be focusable
    current = current?.parentNode;
    while (current && current.tabIndex < 0) {
      current = current.parentNode;
    }
    if (current === document) {
      current = null;
    }
    // return focus to activeElement
    if (!current && document.activeElement && document.activeElement !== document.body) {
      current = document.activeElement;
    }
    if (current) {
      show(current);
    } else {
      hide();
    }
  }
};

В методе show получаем размеры target с помощью getBoundingClientRect. А затем перемещаем, наши 4 div, каждый в свой угол:

const translate = (x, y) => `transform: translate(${x}px, ${y}px)`;

const show = (element) => {
  const rect = element?.getBoundingClientRect();
  let opacity = 0;
  if (rect) {
    opacity = 1;

    leftTop.setAttribute('style', translate(rect.left, rect.top));
    leftBottom.setAttribute('style', translate(rect.left, rect.bottom));
    rightTop.setAttribute('style', translate(rect.right, rect.top));
    rightBottom.setAttribute('style', translate(rect.right, rect.bottom));
  }

  overlay.setAttribute('style', `opacity: ${opacity}`);
};

const hide = () => {
  show();
};

Собственно, всё 😁!

Описанный выше код вы можете найти здесь.

Заключение

Как вы можете заметить, элементы можно стилизовать как хотите, или даже вы можете использовать только один элемент, дав ему тот стиль который вам необходим. Все зависит от вашего воображения.
Кроме того, overflow: hidden не влияет на наш outline, но иногда нам нужно следить за формой элемента и размерами (ResizeObserver) , поэтому вы можете улучшить этот подход, все в ваших руках.

Спасибо за прочтение!

Если вам нужна дополнительная информация, дайте мне знать об этом DM t.me/artzub.

4 комментария 👇
Nikita Galaiko founding software engineer 14 января 2021

Привет!

Я не часто пишу фронтенд и не понимаю много в интерфейсах, поэтому такие посты вызывают вопрос: зачем?

Выглядит это как желание всех, кто в 14 дет поставил линукс сделает его «крутым». Поставить виджетов, анимировать переключение экранов, подсвечивать выделенные пункты в меню.

В итоге ты написал довольно много не самого очевидного кода, на исполнение которого тратятся ресурсы.

Для чего это все?

  Развернуть 1 комментарий

Сейчас стилизация outline сильно ограничена. Можно настроить только цвет, толщину и стиль линии у рамки. Отдельные стороны рамки настроить нельзя, добавить закругления тоже нельзя.

Автор показал, как можно хитренько обойти ограничения. Для дизайна, который он реализует, это оправданно. Обход, как мне кажется, получился не очень сложный, идея и код понятные.

Стилизовать outline действительно нужно, но зачем — это уже отдельная история. Рассказать её быстро и без картинок не получится.

  Развернуть 1 комментарий

@telichkin, Огромное спасибо за ваш коментарий!

  Развернуть 1 комментарий

@ngalaiko, есть производственная необходимость, по этой причине, были проиведены изыскания.
Решения показалось мне простым и понятным, кода совсем немного, и для любого фронтенд разработчика он доступен.

  Развернуть 1 комментарий

😎

Автор поста открыл его для большого интернета, но комментирование и движухи доступны только участникам Клуба

Что вообще здесь происходит?


Войти  или  Вступить в Клуб