Атрибут tabindex

Атрибут для управления порядком фокуса на вашем сайте.

Время чтения: 10 мин

Кратко

Скопировано

Глобальный атрибут, который указывает браузеру, в каком порядке пользователь перемещается между элементами на странице. Может добавить фокус даже для неинтерактивных элементов — параграфов и заголовков.

tabindex так называется из-за клавиши Tab, которой перемещаются между интерактивными элементами. Это действие ещё часто называют «табать» или «протабать».

Пример

Скопировано

Содержимое вкладки добавлено в порядок навигации:

        
          
          <div role="tabpanel" tabindex="0">  <p>Травоядные животные с коротким хоботом, которые живут в лесу.</p></div>
          <div role="tabpanel" tabindex="0">
  <p>Травоядные животные с коротким хоботом, которые живут в лесу.</p>
</div>

        
        
          
        
      

Как пишется

Скопировано

Значение tabindex — любое целое отрицательное или положительное число. Например, 0, 1 или -1.

  • 0 — элемент в последовательном порядке навигации (или фокуса), но сам порядок определяется браузером.
  • 1 или другое положительно число — элемент в порядке навигации без участия браузера. Чем больше значение, тем выше элемент в порядке.
  • -1 или другое отрицательное число — на элемент можно кликнуть, но он не попадает в порядок последовательной навигации. То есть, на нём не сделать фокус с клавиатуры, даже если до этого было можно.

По умолчанию у всех тегов в порядке навигации стоит значение 0. Это <a> или <area> с атрибутом href, <button>, <input>, <select>, <textarea>, первый элемент <summary> внутри <details>, <iframe> и <object>.

Если у нескольких элементов одинаковые положительные значения, они идут по порядку расположения в коде. В этом примере фокус сначала попадёт на ссылку «Усы», потом на «Лапы» и затем на «Хвост».

        
          
          <!-- Пример для наглядности, не делайте так ❌ --><a href="#mustache" tabindex="1">Усы</a><a href="#paws" tabindex="1">Лапы</a><a href="#tail" tabindex="1">Хвост</a>
          <!-- Пример для наглядности, не делайте так ❌ -->
<a href="#mustache" tabindex="1">Усы</a>
<a href="#paws" tabindex="1">Лапы</a>
<a href="#tail" tabindex="1">Хвост</a>

        
        
          
        
      

Когда не задаёте элементам tabindex, браузеры сами выбирают, когда его использовать. Они хорошо с этим справляются без посторонней помощи, но бывают ситуации, когда без tabindex не обойтись.

Несколько основных правил использования tabindex:

  • визуальный порядок навигации должен совпадать с логическим, то есть как расположены элементы в разметке;
  • не изменяйте последовательный порядок навигации без необходимости;
  • добавляйте в порядок навигации только действительно важные элементы, у которых нет такой возможности по умолчанию или это надо изменить;
  • не добавляйте статические элементы вроде параграфов и списков в порядок навигации за редким исключением;
  • не задавайте элементам положительные значения tabindex;
  • дочерние элементы интерактивных родителей не должны быть в порядке навигации — их родитель уже в нём.

Когда нужно убрать интерактивный элемент из порядка навигации, вместо tabindex="0" лучше использовать атрибуты disabled или inert.

Когда использовать

Скопировано

Если нельзя так просто взять и использовать tabindex, зачем атрибут разработчикам? Короткий ответ — для сложных ситуаций, когда нужно управлять порядком навигации вручную.

tabindex помогает сделать удобными сложные компоненты, часто кастомные. Например, модальные окна, древовидные списки, сетки, вкладки и так далее. Также он помогает улучшить пользовательский опыт в формах. К примеру, с помощью tabindex можно переносить фокус на текст ошибки или важную подсказку к полю.

В панели вкладок контейнеру с содержимым с ролью tabpanel часто задают tabindex="0", чтобы пользователи могли попасть в неё с клавиатуры.

        
          
          <div role="tablist">  <button    role="tab"    type="button"    id="tab-1"    aria-selected="true"    aria-controls="tabpanel-1"  >    Тапиры  </button>  <div    role="tabpanel"    id="tabpanel-1"    aria-labelledby="tab-1"    tabindex="0"  >    <p>Травоядные животные с коротким хоботом, которые живут в лесу.</p>  </div></div>
          <div role="tablist">
  <button
    role="tab"
    type="button"
    id="tab-1"
    aria-selected="true"
    aria-controls="tabpanel-1"
  >
    Тапиры
  </button>
  <div
    role="tabpanel"
    id="tabpanel-1"
    aria-labelledby="tab-1"
    tabindex="0"
  >
    <p>Травоядные животные с коротким хоботом, которые живут в лесу.</p>
  </div>
</div>

        
        
          
        
      

Когда в сложных компонентах управляют фокусом с помощью JavaScript-метода focus(), можно сбросить фокус у интерактивного элемента с помощью tabindex="-1". Это часто встречается в кастомных комбинированных и древовидных списках.

В комбинированных списках кнопке с треугольником, которая открывает список по клику, задают tabindex="-1" и убирают из порядка навигации.

        
          
          <label for="input">  Породы собак</label><div>  <input    role="combobox"    id="input"    aria-expanded="false"    aria-controls="listbox"  >  <button    aria-label="Выбрать породу"    aria-expanded="false"    aria-controls="listbox"    tabindex="-1"  ></button></div><ul  role="listbox"  id="listbox"  aria-label="Породы">  <li role="option" id="breed-1">    Алабай  </li>  <li role="option" id="breed-2">    Афганская борзая  </li></ul>
          <label for="input">
  Породы собак
</label>
<div>
  <input
    role="combobox"
    id="input"
    aria-expanded="false"
    aria-controls="listbox"
  >
  <button
    aria-label="Выбрать породу"
    aria-expanded="false"
    aria-controls="listbox"
    tabindex="-1"
  ></button>
</div>
<ul
  role="listbox"
  id="listbox"
  aria-label="Породы"
>
  <li role="option" id="breed-1">
    Алабай
  </li>
  <li role="option" id="breed-2">
    Афганская борзая
  </li>
</ul>

        
        
          
        
      

Обычно в сложных компонентах, как в панели вкладок, используют трюк с перемещающимся tabindex (roving tabindex). Это когда управляем фокусом вручную с помощью JavaScript и меняем значения tabindex в зависимости от того, какой элемент сейчас в фокусе.

        
          
          <div role="tablist">  <!-- Вкладки -->  <!-- Содержимое вкладки, которое по умолчанию в фокусе -->  <div    role="tabpanel"    tabindex="0"  >    <!-- Текст из первой вкладки -->  </div>  <!-- Содержимое вкладки, которое пока что не в фокусе -->  <div    role="tabpanel"    tabindex="-1"  >    <!-- Текст из второй вкладки -->  </div></div>
          <div role="tablist">
  <!-- Вкладки -->

  <!-- Содержимое вкладки, которое по умолчанию в фокусе -->
  <div
    role="tabpanel"
    tabindex="0"
  >
    <!-- Текст из первой вкладки -->
  </div>

  <!-- Содержимое вкладки, которое пока что не в фокусе -->
  <div
    role="tabpanel"
    tabindex="-1"
  >
    <!-- Текст из второй вкладки -->
  </div>
</div>

        
        
          
        
      

Как работает перемещающийся tabindex в сложных компонентах:

  1. У интерактивного элемента, который в фокусе по умолчанию, должен быть tabindex="0". У пока неактивных элементов без фокуса tabindex="-1".
  2. Когда пользователь сделал фокус на другом элементе, для элемента в фокусе по умолчанию устанавливаем tabindex="-1", а для нового — tabindex="0" и фокус при помощи .focus().
  3. Если пользователь вышел из компонента, оставляем tabindex="0" у последнего элемента, с которым он взаимодействовал.

tabindex пригодится и в модальных окнах. Например, когда в окне много текста, первым элементом в фокусе делают первый параграф. Для этого ему тоже задают tabindex="-1". Благодаря этому, если используете клавиатуру, фокус автоматически окажется на этом параграфе при открытии окна.

        
          
          <button aria-controls="modal">Принять условия</button><dialog id="modal" aria-labelledby="label">  <h3 id="label">Условия подписки</h3>  <p tabindex="-1">    Подписываясь на наш сервис, вы обещаете учить язык    не менее 50 часов в день 🦉  </p>  <p>    Если кажется, что это слишком много, идите в другой    сервис. Здесь вам не рады!  </p>  <p>    Смысл начинать учить испанский или китайский,    если откладываете его изучение? К чему бесполезные обещания?  </p>  <form method="dialog">    <button>Принимаю</button>    <button>Убегаю</button>  </form></dialog>
          <button aria-controls="modal">Принять условия</button>
<dialog id="modal" aria-labelledby="label">
  <h3 id="label">Условия подписки</h3>
  <p tabindex="-1">
    Подписываясь на наш сервис, вы обещаете учить язык
    не менее 50 часов в день 🦉
  </p>
  <p>
    Если кажется, что это слишком много, идите в другой
    сервис. Здесь вам не рады!
  </p>
  <p>
    Смысл начинать учить испанский или китайский,
    если откладываете его изучение? К чему бесполезные обещания?
  </p>
  <form method="dialog">
    <button>Принимаю</button>
    <button>Убегаю</button>
  </form>
</dialog>

        
        
          
        
      
Открыть демо в новой вкладке

Кастомные контейнеры со скроллбаром и CSS-свойством overflow — ещё один хороший повод задуматься о tabindex. Без tabindex="0" его нельзя прокрутить с клавиатуры.

        
          
          div[role="group"] {  height: 9rem;  padding: 25px 15px;  overflow: auto;}
          div[role="group"] {
  height: 9rem;
  padding: 25px 15px;
  overflow: auto;
}

        
        
          
        
      
        
          
          <div  role="group"  tabindex="0">  <h3>Условия хранения данных</h3>  <!-- Параграфы с текстом --></div>
          <div
  role="group"
  tabindex="0"
>
  <h3>Условия хранения данных</h3>
  <!-- Параграфы с текстом -->
</div>

        
        
          
        
      
Открыть демо в новой вкладке

Распространённые ошибки

Скопировано

Одна из самых распространённых ошибок — ручной порядок навигации на странице. Это когда у элементов есть tabindex с положительными значениями. Они даже могут повторять порядок расположения элементов в коде:

        
          
          <!-- Не делайте так ❌ --><ul>  <li>    <a href="#mustache" tabindex="1">Усы</a>  </li>  <li>    <a href="#paws" tabindex="2">Лапы</a>  </li>  <li>    <a href="#tail" tabindex="3">Хвост</a>  </li></ul>
          <!-- Не делайте так ❌ -->
<ul>
  <li>
    <a href="#mustache" tabindex="1">Усы</a>
  </li>
  <li>
    <a href="#paws" tabindex="2">Лапы</a>
  </li>
  <li>
    <a href="#tail" tabindex="3">Хвост</a>
  </li>
</ul>

        
        
          
        
      

Ручной порядок навигации может и отличаться от визуального:

        
          
          <!-- Не делайте так ❌ --><ul>  <li>    <a href="#tail" tabindex="3">Хвост</a>  </li>  <li>    <a href="#mustache" tabindex="1">Усы</a>  </li>  <li>    <a href="#paws" tabindex="2">Лапы</a>  </li></ul>
          <!-- Не делайте так ❌ -->
<ul>
  <li>
    <a href="#tail" tabindex="3">Хвост</a>
  </li>
  <li>
    <a href="#mustache" tabindex="1">Усы</a>
  </li>
  <li>
    <a href="#paws" tabindex="2">Лапы</a>
  </li>
</ul>

        
        
          
        
      

В примерах с ручным порядком навигации есть ещё одна ошибка — положительное значение tabindex. Например, когда элемент в конце страницы делают первым в порядке навигации. Из-за этого пользователя неожиданно переносит в конец страницы, а потом снова в начало.

        
          
          <!-- Не делайте так ❌ --><header>  <span>    Блог о сыре  </span>  <nav>    <ul>      <li>        <!-- При втором нажатии на Tab фокус попадёт сюда 2️⃣ -->        <a href="/history/">Приключения сыров в истории</a>      </li>      <li>        <!-- При третьем — сюда и так далее 3️⃣ -->        <a href="/types/">Виды сыров</a>      </li>      <li>        <a href="/degustation/">Дегустация</a>      </li>    </ul>  </nav></header><footer>  <form>    <p>Подпишись на сырную рассылку, чтобы ничего      не пропустить 🧀    </p>    <label for="email">Ваша почта</label>    <!-- При первом Tab фокус попадёт сюда 1️⃣ -->    <input id="email" type="email" tabindex="1">    <button>Подписаться</button>  </form></footer>
          <!-- Не делайте так ❌ -->
<header>
  <span>
    Блог о сыре
  </span>
  <nav>
    <ul>
      <li>
        <!-- При втором нажатии на Tab фокус попадёт сюда 2️⃣ -->
        <a href="/history/">Приключения сыров в истории</a>
      </li>
      <li>
        <!-- При третьем — сюда и так далее 3️⃣ -->
        <a href="/types/">Виды сыров</a>
      </li>
      <li>
        <a href="/degustation/">Дегустация</a>
      </li>
    </ul>
  </nav>
</header>
<footer>
  <form>
    <p>Подпишись на сырную рассылку, чтобы ничего
      не пропустить 🧀
    </p>
    <label for="email">Ваша почта</label>
    <!-- При первом Tab фокус попадёт сюда 1️⃣ -->
    <input id="email" type="email" tabindex="1">
    <button>Подписаться</button>
  </form>
</footer>

        
        
          
        
      

Разработчики часто хотят помочь пользователям и предоставить им одинаковый опыт. К примеру, скринридеры могут перемещаться по заголовкам. Почему бы не добавить заголовки в порядок навигации и не сделать удобно всем? Это приведёт к противоположному результату.

Пользователи, которые видят интерфейс и используют клавиатуру, не ожидают, что могут сделать фокус на… тексте или картинке (кроме редких ситуаций). Это также будет странно и для пользователей скринридеров. Обычно они не ожидают, что у элемента с ролью heading есть фокус.

        
          
          <!-- Не делайте так ❌ --><h1 tabindex="0">  Пара слов обо мне</h1><h2 tabindex="0">  Моя сырная страсть</h2><h3 tabindex="0">  Как всё начиналось</h3>
          <!-- Не делайте так ❌ -->
<h1 tabindex="0">
  Пара слов обо мне
</h1>
<h2 tabindex="0">
  Моя сырная страсть
</h2>
<h3 tabindex="0">
  Как всё начиналось
</h3>

        
        
          
        
      

Другая разновидность ошибки с добавлением неинтерактивного элемента в порядок навигации — кастомные элементы, которые повторяют функциональность существующих HTML-тегов. Например, кастомные кнопки.

        
          
          <!-- Не делайте так ❌ --><div role="button" tabindex="0">Притвориться кнопкой</div>
          <!-- Не делайте так ❌ -->
<div role="button" tabindex="0">Притвориться кнопкой</div>

        
        
          
        
      

Соберём всё самое плохое в одном месте. Попробуйте угадать, что в фокусе, а что нет, и в каком порядке.

Открыть демо в новой вкладке