Damian Słoński

Atrakcyjny i funkcjonalny focus ♿️

Kilka porad dotyczących jednej z podstawowych i często zaniedbywanych funkcjonalności w kontekście a11y.

  • web
  • accessibility
Atrakcyjny i funkcjonalny focus ♿️

Istota stanu zaznaczenia 🥱

Element interfejsu może przyjmować różne stany i w zależności od nich zmieniać swój wygląd. Najbardziej powszechny przykład to :hover na przyciskach albo linkach. Czyli zmiana stanu (co za tym idzie wyglądu, np. koloru) po najechaniu myszką. Taki zabieg poza walorami estetycznymi ma przede wszystkim być dla użytkownika pewną formą wskazówki. "Ten element można kliknąć, więc jeśli teraz kliknę przycisk myszy, to pewnie zostanie wykonana jakaś akcja albo zostanę gdzieś przeniesiony. 🤔" Taki scenariusz jest tak oczywisty, że nikomu nie trzeba tego za bardzo tłumaczyć.

Aczkolwiek :hover to nie jest jedyny dostępny stan przycisku (a ściślej niejedyna pseudoklasa CSS odwołująca się do określonego stanu). Jest ich znacznie więcej i każda z nich ma mniejsze lub większe znaczenie. Z moich obserwacji wynika jednak, że stan :focus, który jest krytycznie ważny z punktu widzenia a11y (accessibility), bywa czasami całkowicie pomijany w stylowaniu. Samo zostawienie go w spokoju nie jest jeszcze najgorszą możliwością, bo przeglądarki dodają do najważniejszych elementów dodatkowe, domyślne stylowanie w przypadku zaznaczenia. Jednak niestety czasami można spotkać się z czymś takim:

1*:focus {
2 outline: none !important;
3 border: none !important;
4 color: inherit !important;
5}

Kod tego typu całkowicie usuwa stan zaznaczenia ze wszystkich elementów, co pod względem accessibility jest katastrofą. Sam temat accessibility, czyli dostępność UI dla osób z niepełnosprawnościami jest wciąż relatywnie niszowy, wymaga pewnej dodatkowej wiedzy i poświęcenia dodatkowego czasu na implementację rzeczy, które nie dotyczą większości użytkowników. Poza tym zabiegi takie często wymuszają pewne ograniczenia w designie i dlatego mogą skutkować gorszym efektem wizualnym produktu.

Jednak zadbanie o podstawowe kwestie dostępności nie jest szczególnie trudne ani pracochłonne, a może wielu osobom ułatwić, albo wręcz umożliwić korzystanie z naszego interfejsu. Jedną z takich podstaw jest :focus, którego w prosty sposób możemy atrakcyjnie ostylować.

Pseudoklasa :focus dotyczy w zasadzie elementów, z którymi użytkownik może wchodzić w interakcje (focusable elements). Są to więc wszelkiego rodzaju przyciski, linki czy elementy formularza. Dla osób, które wspomagają się czytnikami ekranowymi albo nawigują się po stronie wyłącznie za pomocą klawiatury, jest podstawowym narzędziem służącym do poruszania się po stronach. Jego złe obsłużenie przez developera w kodzie można porównać do sytuacji, kiedy np. użytkownik korzystający z myszy nie jest w stanie kliknąć danego przycisku, bo jakiś inny kawałek interfejsu, w wyniku błędu, częściowo na niego najeżdża.

Prosty mixin na ratunek ⛑

Przeglądarki dodają swoje domyślne stylowanie, jednak zazwyczaj nie pasuje ono do brandingu czy style guide'ów naszej strony lub aplikacji. Zwłaszcza że każda przeglądarka dodaje trochę inne stylowanie. Aby to ustandaryzować pomiędzy przeglądarkami i systemami operacyjnymi powinniśmy dodać swoje customowe style. Można takie style dodać globalnie, ale zdecydowanie lepszym rozwiązaniem jest dostosowanie stylowania w zależności od elementu i kontekstu, w jakim się on pojawia. Idealnie sprawdzi się do tego Scss-owy @mixin, który pozwoli nam na jego wielokrotne wykorzystywanie.

1@mixin focus() {
2 &:focus {
3 outline: 1px dashed black;
4 }
5}
6
7.button {
8 @include focus();
9}

Takie rozwiązanie pozwala nam na sporą customizację (używając zmiennych) przy eleganckim zachowaniu zasady DRY (Don't repeat yourself).

1@mixin focus($color: black) {
2 &:focus {
3 outline: 1px dashed $color;
4 }
5}
6
7.button {
8 @include focus();
9 // rest of styles
10}
11
12.button--success {
13 @include focus($color: green);
14}
15
16.hamburger--danger {
17 @include focus($color: red);
18}

Problem z kliknięciem 🖱

Jednym z powodów, dla których stan :focus jest całkowicie usuwany, jest problem, który polega na tym, że w większości przeglądarek jest on aktywowany dodatkowo w momencie pojedynczego kliknięcia myszą. Kiedy użytkownik posługujący się myszką klika przyciski albo linki w nawigacji i w momencie kliknięcia przycisku pojawia się obramowanie, jest to sytuacja w większości przypadków niepożądana, zazwyczaj nie wygląda to zbyt atrakcyjnie i z tego powodu często style dla :focus są całkowicie usuwane. A to błąd! Ponieważ problem ten bez większego wysiłku można rozwiązać na zasadzie: jednocześnie mieć ciastko i zjeść ciastko! 🍪

Zanim przejdziemy do rozwiązania warto zrozumieć działanie pseudoklas :focus oraz :active, ponieważ ich działanie wbrew pozorom nie jest tak trywialne, jakby się mogło wydawać. Dlatego należy zapamiętać te 4 zasady:

  1. Domyślnie stan :active oraz :focus są wyłączone.
  2. Kiedy użytkownik nawiguje się za pomocą klawiatury (klikając klawisz Tab) po focusable elements (<input>, <button>, <textarea>) to otrzymują one stan :focus, bez stanu :active.
  3. Kiedy użytkownik klika po non-focusable elements (<div>, <p>, <span>) to otrzymują one stan :active, bez stanu :focus.
  4. Kiedy użytkownik klika po focusable elements (<input>, <button>, <textarea>) to otrzymują one jednocześnie stan :focus oraz :active.

W przypadku przycisków jest to sytuacja zazwyczaj niepożądana, ponieważ wystarczy nam :active, a :focus jest całkowicie zbędny. Z pomocą przychodzi nam kolejna pseudoklasa :focus-visible.

1@mixin focus($color: black) {
2 &:focus {
3 // reset default browser styles
4 outline: none;
5 }
6
7 &:focus:focus-visible {
8 // add custom styles
9 outline: 1px dashed $color;
10 }
11}

Dzięki wprowadzonemu w specyfikacji Selectors Level 4 pseudoselektorowi :focus-visible możemy przypisać style do elementu, który jest "sfokusowany" oraz w przypadku, co do którego (w zależności od kontekstu) przeglądarka uzna, że powinna je zastosować. Hmmm, ale... to znaczy kiedy?

W gruncie rzeczy to właśnie wtedy, kiedy użytkownik porusza się po interfejsie za pomocą klawiatury oraz gdy element wymaga do działania klawiatury (np. <input>). Czyli: <button> po kliknięciu nie będzie otrzymywał dodatkowych stylów, otrzyma je, gdy użytkownik "złapie go" za pomocą klawiatury. Jednak już element <input> otrzyma dodatkowe style, zarówno po kliknięciu, jak i w przypadku nawigacji przy pomocy klawiatury.

Jednak zachowanie to może zmienić użytkownik (np. ze względu na problemy ze wzrokiem) i z poziomu ustawień przeglądarki ustawić, żeby :focus-visible był aktywowany zawsze, tak samo, jak ma to miejsce w przypadku zwykłej pseudoklasy :focus. Więcej info w specyfikacji W3C.

Jest jedna istotna kwestia w kontekście Safari. Przeglądarka ta na chwilę obecną nie wspiera pseudoklasy :focus-visible, ale również nie "fokusuje" klikniętego przycisku oraz linku. Biorąc pod uwagę te dwa fakty, to posługując się pseudoselektorem :not można stworzyć rozwiązanie, które będzie prawidłowo działało na wszystkich współczesnych przeglądarkach internetowych! 😌

1@mixin focus($color: black) {
2 &:focus {
3 // if focus, add these styles:
4 outline: 1px dashed $color;
5
6 // but if not focus-visible, remove them!
7 // and if you don't know what focus-visible is, remove them too!
8 &:not(:focus-visible) {
9 outline: none;
10 }
11 }
12}

Stosowanie takich selektorów dla każdego elementu z osobna może negatywnie wpływać na czytelność kodu, dlatego zastosowanie tutaj mixina jest według mnie bardzo dobrym rozwiązaniem.

🗓 Update 12.2021

Pseudoklasa :focus-visible została już zaimplementowana w Safari Technology Preview (R122). Zatem w niedalekiej przyszłości nie będzie już potrzeby stosowania przekomplikowanego rozwiązania. 🎉

🗓 Update 03.2022

Pseudoklasa :focus-visible została zaimplenentowana w Safari 15.4, jest teraz oznaczona jako Baseline 2022 i jest dostępna we wszystkich współczesnych przeglądarkach. 🎉

Atrakcyjniejszy focus ✨

Jeśli już wiemy, że nie musimy całkowicie pozbywać się focusa w celu poprawy estetyki UI, to warto też zastanowić się nad tym, co zrobić, aby ten focus mógł wyglądać nieco lepiej? Przede wszystkim warto skorzystać z właściwości outline-offset, która pozwala odsunąć outline od elementu, dodając trochę bezcennego światła. Zastosowanie tylko tej jednej rzeczy może diametralnie poprawić wrażenia estetyczne. Do tego warto stosować pasujący do elementu i kontekstu kolor oraz korzystać z kropkowanego dotted lub kreskowanego dashed obramowania.

outline-focus 0px vs outline-focus 4px
outline-focus 0px vs outline-focus 4px

A dzięki naszemu mixinowi możemy to zrobić bardzo prosty i customizowalny sposób!

1@mixin focus($color: black, $offset: 4px) {
2 &:focus {
3 outline: 1px dashed $color;
4 outline-offset: $offset;
5
6 &:not(:focus-visible) {
7 outline: none;
8 }
9 }
10}
11
12.button {
13 @include focus();
14}
15
16.button--large {
17 @include focus($offset: 8px);
18}

Podsumowanie 📋

Jeśli kiedyś zdarzyło Ci się, czy to na prośbę klienta, czy z własnej inicjatywy całkowicie usunąć stylowanie dla stanu :focus to znając pseudoklase :focus-visible oraz właściwość outline-offset nie musisz tego już więcej robić!

Te kilka dodatkowych linijek kodu może okazać się naprawdę niezwykle przydatnych dla większości osób z niepełnosprawnościami, które używają alternatywnych sposobów nawigacji po interfejsie.