НачалоОсновыКомпонентыТексты

Фокус

Фокус с клавиатуры виден всегда. Поле подсвечивает собственную рамку, любой другой контрол получает кольцо вокруг себя.

РуководствоТокены
Save Remember me Notifications
<tk-button>Save</tk-button>
<tk-checkbox>Remember me</tk-checkbox>
<tk-switch>Notifications</tk-switch>

Пройдите по примерам на этой странице клавишей Tab. Фокус показывается для клавиатуры (:focus-visible), а не после щелчка мышью.

Поля: рамка

У поля для ввода текста — инпута, селекта, выбора даты — кольца нет. В фокусе его рамка окрашивается в цвет фокуса и утолщается на 0,5px внутрь, поэтому на странице ничего не сдвигается.

<tk-field label="Email address">
  <tk-input type="email" name="email"></tk-input>
</tk-field>
<tk-field label="Promo code" error="This code has expired">
  <tk-input name="promo" value="SPRING"></tk-input>
</tk-field>
Состояние Рамка
покой border/default, 1px
hover border/strong, 1px
фокус border/accent/focus, 1.5px
ошибка border/error, 1px
ошибка + фокус border/error, 1.5px
только чтение border/subtle, 1px; фокус показывается
отключено border/disabled, 1px; фокус не принимает
  • Ошибка в фокусе остаётся красной. Рамка утолщается, и человек видит и где он печатает, и что не так.
  • Фокус важнее hover. Поле в фокусе сохраняет рамку фокуса и под указателем.

Всё остальное: кольцо

Кнопки, ссылки, чекбоксы, вкладки, слайдеры, карточки и все прочие контролы получают кольцо в 2px на расстоянии 2px от края. В зазоре видна страница, поэтому кольцо заметно и на залитом контроле.

Publish Delete
<tk-button variant="primary">Publish</tk-button>
<tk-button tone="danger">Delete</tk-button>
<tk-icon-button label="Settings"><tk-icon name="settings"></tk-icon></tk-icon-button>

Кольцо одного цвета у всех вариантов и тонов: у опасной кнопки оно не красное. Кольцо говорит, где клавиатура, а не что значит контрол.

Фокус на ваших контролах

Берите токены и нативный outline:

.my-control:focus-visible {
  outline: var(--focus-ring-width) solid var(--border-accent-focus);
  outline-offset: var(--focus-ring-offset);
}
  • :focus-visible, а не :focus. Исключение одно: активный пункт меню или списка с перемещаемым фокусом может подсвечиваться по :focus, потому что подсветка нужна и после щелчка.
  • Не убирайте outline без замены.
  • Токены, а не 2px, — тогда ваши контролы совпадут с остальными.

Угол кольца

outline сам повторяет border-radius элемента, поэтому радиус кольцу не задают. Угол кольца — это радиус контрола плюс отступ 2px: контрол с радиусом 8px получает кольцо 10px, карточка 12px — 14px. Если кольцо всё же рисуется через box-shadow, задайте этот радиус сами: calc(var(--radius-8) + var(--focus-ring-offset)).

Внутреннее кольцо

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

  • тонкая ручка, где кольцо было бы больше самой цели;
  • область прокрутки, таблица или блок кода, чей край совпадает с краем компонента, так что родитель с overflow обрежет внешнее кольцо;
  • ячейка плотной сетки, например день в календаре, где кольцо залезет на соседей;
  • маленькая кнопка внутри поля, где внешнее кольцо обведёт рамку поля.
.my-handle:focus-visible {
  outline: var(--focus-ring-width) solid var(--border-accent-focus);
  outline-offset: calc(var(--focus-ring-offset) * -1);
}

Меняйте знак у токена отступа, а не толщины. Угол внутреннего кольца — угол самого контрола.

Когда кольцо перекрывают

Outline рисуется вместе со своим элементом, и стереть его могут две вещи:

  • позиционированный потомок (sticky, absolute, даже relative), например липкая шапка, рисуется поверх кольца;
  • mask, opacity или filter на элементе гасят кольцо вместе с ним.

Решение: рисуйте кольцо на пустом соседнем элементе поверх области, а саму область сделайте контекстом наложения.

.region { position: relative; }
.region > .scroll { isolation: isolate; }
.region > .ring { position: absolute; inset: 0; pointer-events: none; }
.scroll:focus-visible { outline: none; }
.scroll:focus-visible ~ .ring {
  outline: var(--focus-ring-width) solid var(--border-accent-focus);
  outline-offset: calc(var(--focus-ring-offset) * -1);
}

Области прокрутки Thykes (Scroll area, Table, Data table, Code block, Sidebar) уже устроены так.

Инверсные поверхности

На инверсной поверхности (surface/inverse: тёмной в светлой теме, светлой в тёмной) цвет фокуса почти не виден. Переназначьте токен фокуса на border/inverse на самой поверхности, и все контролы внутри, включая вложенные вами, получат кольцо инверсного цвета:

.my-inverse-panel { --border-accent-focus: var(--border-inverse); }

Тосты и громкие баннеры уже так делают, а у tk-link есть color="inverse".

A new version is available. Reload
<tk-banner treatment="loud" label="Update" dismissible>
  A new version is available.
  <tk-button slot="actions" size="small">Reload</tk-button>
</tk-banner>

Принудительные цвета

В режиме высокой контрастности Windows (forced-colors: active) браузер убирает box-shadow и красит все рамки одним системным цветом, так что рамка фокуса у поля пропадает. Поля Thykes добавляют там настоящий outline системного цвета Highlight. Кольца и так outline, им ничего не нужно. Если делаете своё поле, сделайте так же:

@media (forced-colors: active) {
  .my-field:focus-within {
    outline: var(--focus-ring-width) solid Highlight;
    outline-offset: var(--focus-ring-offset);
  }
}

Доступность

Уже встроено:

  • Видимый индикатор фокуса у всего, что принимает фокус с клавиатуры.
  • Фокус не сдвигает вёрстку: рамка поля утолщается внутрь, а outline места не занимает.
  • Индикатор, который виден в режиме высокой контрастности Windows.
  • Поля только для чтения принимают фокус и показывают его; отключённые — не принимают.

Ваша часть:

  • Сохраняйте индикатор у всего, что стилизуете или делаете сами; никогда не ставьте outline: none без замены.
  • На своей инверсной поверхности переназначьте --border-accent-focus на border/inverse.
  • Не давайте липкому потомку, маске или overflow спрятать кольцо; проверяйте с клавиатуры.

Ограничения контраста

Кольцо — основного цвета (primary/500 в светлой и тёмной теме, primary/800 в контрастной), так что его контраст зависит от выбранного основного цвета. Минимум для нетекстовых элементов — 3:1.

  • Светлая: синий, индиго и фиолетовый проходят на всех поверхностях. Оранжевый не проходит ни на одной (2.20:1 на белом). Серый не проходит на surface/subtle (2.9993:1) и surface/sunken; зелёный, мятный, бирюзовый и коричневый — на surface/sunken.
  • Тёмная: везде проходит только оранжевый. Синий — 2.38:1 на surface/subtle и 3.32:1 на surface/raised, панели каждого диалога, меню и поповера. Индиго не проходит на обеих.
  • Высокий контраст: проходит для любого оттенка на любой поверхности.

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

Можно / Нельзя

✅ Можно ❌ Нельзя
Показывать фокус поля его рамкой Давать полю и кольцо, и смену рамки
Брать outline с токенами фокуса Писать outline: 2px solid …
Оставлять ошибку красной в фокусе Перекрашивать ошибку в цвет фокуса
Брать :focus-visible Рисовать кольцо по голому :focus
Менять знак у --focus-ring-offset для внутреннего кольца Менять знак у --focus-ring-width
Рисовать кольцо на соседнем слое, если потомок позиционирован Рисовать его на элементе с липким потомком
Переназначать токен фокуса на border/inverse на инверсной поверхности Оставлять основное кольцо на инверсной поверхности