Keyboard-Only Focus

One of the things I couldn't man­age to find a so­lu­tion for a long time was a prob­lem of fo­cus styles on in­ter­ac­tive el­e­ments. The prob­lem was: when you have an el­e­ment with some :fo­cus styles, they're ap­plied not only to the fo­cused state it­self but also af­ter you just click on this el­e­ment (and it be­haves dif­fer­ently for dif­fer­ent el­e­ments in dif­fer­ent browsers, of course).

What this meant is that when­ever you wanted to have some cus­tom el­e­ment, you would need to com­pro­mise in how the fo­cus state would look like, be­cause if you'd make it too bright or dis­tinct from the nor­mal state, users would see it when­ever they'd click the el­e­ments with those styles. So you're ei­ther get­ting the too no­tice­able ef­fect when it is not needed or not enough no­tice­able ef­fect when it is needed. Ac­ces­si­bil­ity-wise, the for­mer is, of course, bet­ter, but what if we could make those styles not to ap­ply when you click at all?

In this post, I'll pre­sent to you the so­lu­tion (You can jump straight to the fi­nal so­lu­tion, but I rec­om­mend you to read the whole post any­way in or­der to bet­ter un­der­stand it.) I had found. It is not per­fect, but I think this is enough for us to start mak­ing our el­e­ments both hav­ing a dis­tinct bright fo­cus state and be good look­ing when users would use a mouse, a track­pad etc.

Current State of the Problem

To un­der­stand the prob­lem bet­ter, we should look at how things be­have in dif­fer­ent browsers for the sim­plest test case.

Try to hover, click and switch fo­cus over those con­trols:

I'm a link! I'm a span!

Browsers (Note that the Sa­fari doesn't get key­board fo­cus for links by de­fault (you need to en­able it in its pref­er­ences for this), but still gets the state on click.) be­have a bit dif­fer­ently there:

Which Browsers Get the Focus Ring After Click

Browser But­ton Link Span
Chrome Yes Yes Yes
Edge Yes Yes Yes
Fire­fox Win Yes Yes Yes
Fire­fox Mac No Yes Yes
Sa­fari No Yes* Yes

We can al­ready see that for most el­e­ments in all browsers we would get that fo­cused state when we'd click our cus­tom el­e­ment, while for the <but­ton>s in Sa­fari and Fire­fox for Mac we won't.

Possible Solution in the Specs?

One thing we should al­ways do when we start ex­per­i­ment­ing is to see if there is al­ready a so­lu­tion in cur­rent or in­com­ing spec­i­fi­ca­tions.

The lat­est draft of Se­lec­tors Level 4 pro­vides us with two new fo­cus-re­lated pseudo-classes: fo­cus-within and fo­cus-ring.

  • :fo­cus-within is es­sen­tially a :fo­cus that works like :hover —when­ever chil­dren of an el­e­ment would get :fo­cus, the el­e­ment would get :fo­cus-within (and the el­e­ment would get it when fo­cused it­self, of course). I can see how that would be a re­ally use­ful tool in fu­ture, but for our case it's rather use­less (un­less we'd try to do some re­ally wild things).
  • :fo­cus-ring seems like a tool made specif­i­cally for our use-case —it is kinda the old :fo­cus, but with this added: […] and the UA de­ter­mines via heuris­tics that the fo­cus should be spe­cially in­di­cated on the el­e­ment.

In the­ory, :fo­cus-ring should help us (Of course, we'd need to im­ple­ment it us­ing pro­gres­sive en­hance­ment: de­clar­ing all the styles for :fo­cus, then re­mov­ing the styles on :not(:fo­cus-ring), as oth­er­wise, we would lose in key­board ac­ces­si­bil­ity at older browsers.): we could use it for key­board fo­cus styles while leav­ing the nor­mal :fo­cus with­out any­thing ex­cess. But there are two prob­lems: no browser, as far as I know, sup­ports the prop­erty from the spec and only Fire­fox sup­ports its :-moz-fo­cus­ring —old pro­pri­etary pseudo-class, which was a base for a new one. And this pseudo-class is al­ready a bit flawed:

I'm a link! I'm a span!

If you'd look at this ex­am­ple in Fire­fox, you could see two dif­fer­ent issues:

  1. At Fire­fox for Mac it would work prop­erly un­less you'd click on a styled span, in which case you'll see the fo­cus-ring. That's un­for­tu­nate, as while for proper na­tive con­trols things are ok, I'm sure there would be cases when peo­ple would like to have this kind of con­trol over in­ter­ac­tive parts of pages that should have the same be­hav­ior as the na­tive con­trols. But as things are work as in­tended at Win­dows, maybe that be­hav­ior at Mac is a bug? Prob­a­bly we could see it fixed in fu­ture.
  2. This is­sue is big­ger and worse —as Patrick has men­tioned, there is a re­ally weird heuris­tic which makes the :-moz-fo­cus­ring to ap­ply all-the-time once you've used a key­board nav­i­ga­tion on page at least once. That makes this so­lu­tion not sta­ble enough. Es­pe­cially, given that in some cases I man­aged to “break” Fire­fox mak­ing it to con­sider every page to be in this key­board-nav­i­ga­tion al­ways-on from the start and on re­fresh, which was fixed only af­ter the browser restart.

In other browsers, you shouldn't see any­thing there un­less some­one would al­ready im­ple­ment the :fo­cus-ring. If that would hap­pen —tell me and I'll up­date this post!

My Initial Solution

I was play­ing with one of my fa­vorite CSS prop­er­ties —vis­i­bil­ity —when I had my “bingo” mo­ment. Af­ter val­i­dat­ing my idea and see­ing it work I was re­ally sur­prised I didn't come to this so­lu­tion be­fore. Af­ter some test­ing (Ap­par­ently, I didn't test enough :( As Ian pointed out af­ter I ini­tially pub­lished this post, I didn't test the so­lu­tion enough at Fire­fox for Win­dows. That meant two things: at Win­dows the <but­ton> still had the fo­cus­ring vis­i­ble, and I didn't know about the heuris­tic of the :-moz-fo­cus­ring that made the but­ton with my so­lu­tion com­pletely un­us­able af­ter user would use key­board for at least once. I leave this so­lu­tion to show what could be done if not for Fire­fox, and in­vite you to look at the fi­nal proper so­lu­tion be­low.), I found out that not every­thing is so smooth, but more on this later.

Visibility

I think this prop­erty de­serves its own sep­a­rate ar­ti­cle just to show all the things you can do with it. But for now I'll briefly tell which its fea­tures would be re­ally use­ful for our case:

  1. When­ever you have an el­e­ment with vis­i­bil­ity: hid­den, you can use vis­i­bil­ity: vis­i­ble on its chil­dren to make them vis­i­ble.
  2. When­ever you hide an el­e­ment us­ing vis­i­bil­ity: hid­den, it would not only be­come vi­su­ally in­vis­i­ble (like with opac­ity), but it wouldn't also get key­board fo­cus and wouldn't be vis­i­ble to as­sis­tive tech­nolo­gies. You can guess that the part about the key­board fo­cus is what in­ter­ests us there.

You could al­ready see where all of this leads us to: what if we'll add an­other el­e­ment in­side our in­ter­ac­tive el­e­ment, then use vis­i­bil­ity: hid­den on the el­e­ment it­self, but then re­turn its con­tents back with vis­i­bil­ity: vis­i­ble?

The an­swer: the el­e­ment would be­come ac­ces­si­ble only us­ing point­ing-and-click­ing de­vices, as click­ing on the in­sides of a hid­den el­e­ment still trig­gers all the events on it. Of course, that's not ex­actly what we need, as we ac­tu­ally need the key­board fo­cus and don't want to lose in ac­ces­si­bil­ity in any way.

When to Hide

But then I was won­der­ing: what if we would use this method of hid­ing-and-show­ing only when the pointer de­vice is used? The pseudo-classes :hover and :ac­tive come to mind.

  • If we'd use :hover there, every­thing would work al­most as we would in­tend, but there would be a small is­sue of when we'd hover an el­e­ment, and then we'd want to use key­board nav­i­ga­tion, we couldn't get a fo­cus on this el­e­ment.
  • If we'd use :ac­tive, then things would al­most work, but the prob­lem would be that when you call an ac­tion on an in­ter­ac­tive el­e­ment us­ing a key­board, then some browsers ac­tu­ally ap­ply :ac­tive state on this el­e­ment. The ac­tion would pass, but at the same time, we'll lose the fo­cused state on the el­e­ment.

Af­ter play­ing a bit with dif­fer­ent com­bi­na­tions, the most log­i­cal way would be to use both states: :hover:ac­tive. Un­less we in­ter­act with an el­e­ment with some­thing that is ac­tu­ally point­ing it we won't trig­ger this state. With it, our so­lu­tion would look like this:

.Button:active:hover {
    visibility: hidden;
}

:active:hover > .Button-Content {
    visibility: visible;
}

Of course, in that case, we'd need to move all the vi­sual styling of the but­ton to the in­ner el­e­ment, and use the par­ent el­e­ment only for layout.

And then when we'd need to de­clare all the states like :hover, :ac­tive and :fo­cus, we'd need to do them us­ing se­lec­tors (Note that we don't need to add the par­ent's el­e­ment class to the state when it is used like that —there are no per­for­mance is­sues due to how the se­lec­tor match­ing works in browsers.) like :fo­cus > .But­ton-Con­tent.

Here, look at this ex­am­ple (bet­ter not in Fire­fox for now):

I'm a link! I'm a span!

We're al­most there! If you'd look at this ex­am­ple in Chrome or Edge, every­thing would work per­fectly: when you'd click those but­tons, they would work and won't be­come vis­i­bly fo­cused, but you would still be able to fo­cus them from the key­board and see the fo­cus ring.

How­ever, there are a few is­sues in other browsers: Sa­fari and Fire­fox. In Sa­fari the first but­ton (the one that is made us­ing <but­ton>) would sud­denly have the fo­cus state clipped as if the par­ent but­ton had over­flow: hid­den (while this is not the case). This is eas­ily fixed by adding po­si­tion: rel­a­tive on the in­ner el­e­ment which some­how fixes this bug.

But in Fire­fox… Well, our :hover:ac­tive stuff just doesn't work. And even more to it, if we'd ac­tu­ally lis­ten to the but­ton's events, then on <but­ton> we won't get (I sus­pect that there is a re­lated bug in bugzilla for this, but maybe there are oth­ers, feel free to find if some other bug fits bet­ter!)click when we click (and that should be a click, not a tap!). That only hap­pens when you change the vis­i­bil­ity of a <but­ton> on the :ac­tive state.

Fixing Firefox

Up­date from 28 June 2017: At the time of writ­ing this part I didn't test things in Fire­fox for Win. Af­ter test­ing it there I found out that this so­lu­tion is com­pletely un­us­able due to var­i­ous prob­lems in Fire­fox for Win­dows' im­ple­men­ta­tion of but­tons. Do not use this fix.

Af­ter spend­ing a lot of time on try­ing to find a fix for Fire­fox, the best fix I could find was this:

  • The <but­ton> it­self ac­tu­ally worked as in­tended al­ways in Fire­fox, so we don't need to do any­thing for it.

    So, only for Fire­fox, we'd need to re­store its vis­i­bil­ity for the ac­tive state:

      button.Button:active:not(:-moz-focusring) {
         visibility: inherit;
      }
    

    Two things to note there:

    1. We're restor­ing the vis­i­bil­ity us­ing in­herit, as in all cases when you need to re­store the vis­i­bil­ity it's bet­ter to use in­herit in­stead of vis­i­ble, be­cause oth­er­wise in­side a vis­i­bil­ity: hid­den con­text we would sud­denly make our el­e­ment vis­i­ble. In our case, it doesn't mat­ter much, but it's just a good prac­tice to get used to.
    2. For ap­ply­ing styles just for Fire­fox I'm us­ing the pseudo-class that only Fx un­der­stands —:-moz-fo­cus­ring, wrap­ping it with :not() as that's the case when we'd want it to work.
  • For in­ter­ac­tive links, spans and other non-but­ton el­e­ments we'd need to use our method on :hover, which is not ideal as I men­tioned be­fore, but what can we do? At least, we can add an ex­tra guard not to hide el­e­ments on hover when they're al­ready fo­cused. And we'd also use the same :not(:-moz-fo­cus­ring) hack, and it even would make some sense there!

      .Button:not(button):hover:not(:focus):not(:-moz-focusring) {
          visibility: hidden;
      }
    
      :not(button):hover:not(:focus):not(:-moz-focusring) > .Button-Content {
          visibility: visible;
      }
    

    Note that we do this not for but­tons, as adding this stuff for them on hover would make them flicker, and we al­ready have na­tive but­tons in Fx to be­have like we want them to.

Final Initial Solution

With all those fixes, our ex­am­ple would look like this and should work the same in all mod­ern browsers:

I'm a link! I'm a span!

But not that fast. In Fire­fox for Win­dows there are se­vere prob­lems with but­tons' im­ple­men­ta­tion which lead to this so­lu­tion be­ing un­us­able. Thanks for Ian De­vlin for point­ing this out. In Fire­fox for Win­dows you still would see the fo­cus ring over the <but­ton> there on click, and af­ter you'd use key­board nav­i­ga­tion at least once, things would go com­pletely wrong —Fire­fox would treat each :fo­cus as if it has :-moz-fo­cus­ring, which would be mul­ti­plied by an­other prob­lem with but­tons which would lead to <but­ton> not to reg­is­ter clicks.

You can see how this ini­tial so­lu­tion worked on this Code­Pen pen, with­out any ex­tra styling (ex­cept for all: ini­tial on but­tons).

Proper Solution

Af­ter writ­ing and pub­lish­ing this ar­ti­cle, and then find­ing out the ac­tual so­lu­tion just don't work in Fire­fox for Win­dows (and not just not work —in some con­di­tions it makes the but­tons in­ac­ces­si­ble), I've tried to find a proper so­lu­tion. And, I think, I found it! Still not ideal at Fire­fox for Win­dows, but with­out those se­vere prob­lems. I hope.

The so­lu­tion came af­ter watch­ing this video by Rob Dob­son that Vadim Ma­keev had sug­gested to me and read­ing this ar­ti­cle about tabindex that was sug­gested by Patrick H. Lauke.

Look at it:

I'm a link! I'm a span!

If should work every­where ex­cept for one case: only in Fire­fox for Win­dows af­ter you'll use key­board nav­i­ga­tion at least once, you'll see fo­cus ring af­ter click­ing on the <but­ton> el­e­ment. Maybe it is pos­si­ble to fix this case, but its not crit­i­cal (Every­thing would work prop­erly ex­cept for the vi­sual fo­cus ring even in this case, not like with the pre­vi­ous so­lu­tion, and its the Fire­fox that should start with fix­ing its own prob­lems with the <but­ton>, not us find­ing hacky workarounds for them.).

The fun thing: this so­lu­tion is even sim­pler than the pre­vi­ous one.

  1. We're just adding a tabindex="-1" to the in­ner el­e­ment, which makes all browsers to set ac­tual fo­cus not on the in­ter­ac­tive el­e­ment it­self, but on its in­sides. And when an el­e­ment has fo­cus in­side of it, all the key­board events would still work prop­erly, so you could “click” this but­ton from key­board with­out any prob­lems. This is pos­si­ble due to how browsers han­dle that neg­a­tive tabindex —they don't put el­e­ments with it to the key­board fo­cus list, but make it pos­si­ble to fo­cus those el­e­ments by click­ing on them or by set­ting fo­cus pro­gram­mat­i­cally.
  2. The only place where this fix doesn't work is Fire­fox for Win­dows. There, only for a <but­ton> el­e­ment, you would still see the fo­cus ring. I've fixed it us­ing the :-moz-fo­cus­ring by re­mov­ing the fo­cus styles when we don't need them. Of course, due to Fire­fox' strange heuris­tic, af­ter you'll use key­board nav­i­ga­tion at least once, you'll then see this fo­cus­ring on click, but that's a mi­nor draw­back and every­thing would still work oth­er­wise.

Here is an HTML sim­i­lar (Here and in the later CSS ex­am­ple I used more sim­pli­fied class­names and over­all changed things a bit for them to be more read­able, as well as not men­tion­ing all the re­set and vi­sual styles for our but­tons.) to the one used for this so­lution:

<button class="Button" type="button" tabindex="0">
    <span class="Button-Content" tabindex="-1">
        I'm a button!
    </span>
</button>

<a class="Button" href="#x" tabindex="0">
    <span class="Button-Content" tabindex="-1">
        I'm a link!
    </span>
</a>

<span class="Button" tabindex="0">
    <span class="Button-Content" tabindex="-1">
        I'm a span!
    </span>
</span>

And here is the fi­nal CSS (You can see how it works with this lit­eral CSS on this Code­Pen pen, with­out any ex­tra styling (ex­cept for all: ini­tial on but­tons).) for our method:

/* Fixing the Safari bug for `<button>`s overflow */
.Button-Content {
    position: relative;
}

/* All the states on the inner element */
:hover > .Button-Content {
    background: blue;
}

:active > .Button-Content {
    background: darkorange;
}

:focus > .Button-Content {
    box-shadow: 0 0 3px 7px lime;
}

/* Removing the focus ring styles specifically for button in Firefox */
button:not(:-moz-focusring):focus > .ComplexButton-Content {
    box-shadow: none;
}

/* Removing default outline only after we've added our custom one */
.Button:focus,
.Button-Content:focus {
    outline: none;
}

Note that while the par­ent el­e­ment is not hid­den at any mo­ment un­like at the pre­vi­ous so­lu­tion with vis­i­bil­ity, it is still bet­ter to have all the styles for our el­e­ment on the in­ner el­e­ment due to var­i­ous reasons.

As an added bonus, if we'd want, we can now choose how we'd want to have our el­e­ment when fo­cus­ing it pro­gram­mat­i­cally: we can have a vi­sual fo­cus by fo­cus­ing an outer el­e­ment, or we can choose not to have fo­cus by fo­cus­ing the in­ner el­e­ment. Even more to it: if we'd add an­other in­ner el­e­ment with the tabindex="-1", then we could style el­e­ment dif­fer­ently now in three cases: on key­board fo­cus, on click fo­cus and on pro­gram­matic focus.

Remaining Problems

  • In Fire­fox for Win­dows af­ter us­ing the key­board nav­i­ga­tion at least once we would see the fo­cus ring over the <but­ton> el­e­ments. That's un­for­tu­nate, but it doesn't break the func­tion­al­ity and should be a rather rare and mi­nor case.
  • in IE11 the so­lu­tion would work for links and other el­e­ments, but you'll al­ways see the fo­cus ring for <but­ton>.
  • This method needs an ex­tra el­e­ment and won't work for <in­put type="but­ton"> ob­vi­ously (which I don't rec­om­mend us­ing for but­tons any­way) and there is lit­er­ally noth­ing bad in adding an ex­tra span when it helps us to fix a prob­lem. Ex­tra divs and spans are not not se­man­tic.

So, not a lot of prob­lems af­ter all. If you'll see any­thing in the fi­nal ex­am­ple in the browser you use, let me know!

Final Words

Now we can use a nice and no­tice­able key­board fo­cus state for our in­ter­ac­tive el­e­ments with­out com­pro­mis­ing in how it would look for oth­ers. That's the thing I tried to fix for years and I don't even re­mem­ber the num­ber of at­tempts I did to fix it. I'm re­ally glad I fi­nally nailed it, and I hope browsers would start sup­port­ing the :fo­cus-ring pseudo-class, and that it wouldn't have that an­noy­ing heuris­tic that it has now and would work for any el­e­ments that have a set tabindex.

Thank you for reading.