Фокус
Фокус с клавиатуры виден всегда. Поле подсвечивает собственную рамку, любой другой контрол получает кольцо вокруг себя.
<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 от края. В зазоре видна страница, поэтому кольцо заметно и на залитом контроле.
<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".
<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 на инверсной поверхности |
Оставлять основное кольцо на инверсной поверхности |
Три длины рисуют любой индикатор фокуса; красит его один цветовой токен.
Геометрия
Одинакова во всех темах.
| Токен | CSS-переменная | Значение | Для чего |
|---|---|---|---|
focus-ring-width |
--focus-ring-width |
2px | толщина кольца (outline-width) |
focus-ring-offset |
--focus-ring-offset |
2px | зазор между контролом и кольцом (outline-offset); со знаком минус — внутреннее кольцо |
focus-border-inset |
--focus-border-inset |
0.5px | на сколько рамка поля в фокусе утолщается внутрь |
Цвет
| Токен | Светлая | Тёмная | Высокий контраст | Для чего |
|---|---|---|---|---|
border/accent/focus |
primary/500 |
primary/500 |
primary/800 |
кольцо и рамка поля в фокусе |
border/inverse |
gray-50 |
gray-950 |
gray-50 |
кольцо на инверсной поверхности (surface/inverse) |
border/accent/focus следует за выбранным основным цветом. Ограничения контраста — в
Как они сочетаются
Кольцо:
.my-control:focus-visible {
outline: var(--focus-ring-width) solid var(--border-accent-focus);
outline-offset: var(--focus-ring-offset);
}Поле в фокусе:
.my-field:focus-within {
border-color: var(--border-accent-focus);
box-shadow: inset 0 0 0 var(--focus-border-inset) var(--border-accent-focus);
}