# Managing focus in SVG
This document explains how managing focus in SVG works and how it differs from HTML.
# SVG specifications
There are three relevant iterations of SVG considering focus management: SVG 1.1, SVG 1.2 Tiny and SVG 2. Although SVG 1.1 knew focus events such as focusin
and focusout
and mentions the CSS pseudo-class :focus
, it does not describe how elements can be made focusable. SVG 1.2 Tiny rectifies that by adding the focusable
attribute. And finally SVG 2 defines focus management to be the same as HTML, including the tabindex
attribute.
# Browser compatibility
While the Blink and WebKit based browsers (Google Chrome, Apple Safari) have had solid support for :focus
, .focus()
and tabindex
on SVG elements for quite a while, the story is different for Gecko (Mozilla Firefox), Trident (Microsoft Internet Explorer) and EdgeHTML (Microsoft Edge).
Up to version 50 Firefox had very limited support for focus in SVG. <a xlink:href="…">
elements could be focused by keyboard and mouse, but the .focus()
method was missing. The only way to shift focus to an SVG element was by user action (keyboard, pointer). As of version 51 Firefox has caught up.
Internet Explorer is the only mainstream browser to implement SVG 1.2 Tiny's focusable
attribute to make arbitrary elements focusable. However, it does not support the tabindex
attribute and .focus()
method on SVGElement
s. Support for focusable
and the lack of .focus()
carried over to Microsoft Edge, which only added support for the tabindex
attribute in version 14.
# Making SVG elements focusable
In SVG only the element <a xlink:href="…">
is considered naturally focusable by all browsers. Internet Explorer and Microsoft Edge up to version 13 also consider <svg>
itself keyboard focusable. Firefox 52 considers <svg>
focusable, if it's the root element of a browsing context (e.g. <iframe>
or <object>
).
Browsers on macOS in general and Apple's Safari in particular need to be configured for keyboard navigation.
The HTML attribute tabindex
made its way into SVG in SVG 2. The behavior of the attribute is defined to be the same as in HTML, i.e. a negative value making elements script and pointer focusable, a value of 0
adding the element to the document's tabbing order, and a positive value affecting the order in the document's sequential focus navigation list.
Don’t Use Tabindex Greater than 0 also applies to SVG content.
The focusable
attribute defined by SVG Tiny 1.2 is only implemented in Internet Explorer and Microsoft Edge. Unlike tabindex
this attribute has a boolean value, where focusable="true"
equals tabindex="0"
and focusable="false"
makes the element inert. Besides removing SVG links from the tabbing order by way of <a xlink:href="…" focusable="false">
, this property also comes in handy to prevent the SVG's root element from unnecessarily receiving focus: <svg focusable="false">
.
In general it's not a good idea to remove interactive elements from the tabbing order, such as <a href="…" tabindex="-1">
would achieve. However, this is the only way for utilities like ally.element.disabled
and the WICG inert
polyfill to prevent elements from receiving keyboard focus, while maintaining their visual presentation on screen.
As of version 14 Microsoft Edge also supports the tabindex
attribute. For the case <rect … tabindex="0" focusable="true">
the situation is clear: the element element is keyboard focusable. However both attributes could be contradicting each other, in which case the focusable
attribute wins and the tabindex
attribute is ignored:
<svg focusable="false" tabindex="0">
is considered inert<svg focusable="true" tabindex="-1">
is considered keyboard focusable
# Event Listeners
SVG2 - 15.9. Focus: […] may allow focus to reach elements which have been assigned listeners for mouse, pointer, or focus events.
Blink and WebKit (Chrome and Safari) consider any SVG element with a focus
event handler focusable. However, mouse events (e.g. click
, mousedown
) and keyboard events (e.g. keydown
) don't cause elements to become focusable.
Since we can't query the event listeners attached to DOM elements, it's impossible to identify elements that have become focusable by way of a focus
event listener.
Any element that has a focus
event listener attached should also be made focusable by adding tabindex="0"
.
# Focusing SVG elements
In Blink and WebKit based browsers focusing SVG elements is not different from focusing an HTML element, the .focus()
method is available on HTMLElement
and SVGElement
. Since Firefox 51 the .focus()
method is also available. In Internet Explorer and Microsoft Edge this is not the case.
Internet Explorer and Microsoft Edge up to version 12 allow misappropriating the .focus()
method by applying the method from HTMLElement
's prototype to an SVGElement
: HTMLElement.prototoype.focus.apply(svgElement)
. Microsoft Edge 13 joined Firefox (up to version 50) in rejecting such attempts by throwing an error:
// Firefox
TypeError: 'focus' called on an object that does not implement interface HTMLElement.
// Edge
TypeError: Invalid calling object
While Firefox before version 51 can't focus SVG elements because of this limitation, Microsoft Edge 13 and newer can be persuaded otherwise.
# The focus SVG element hack
Mutating the active element shows that Edge will shift focus to the next focusable ancestor when the active element is disabled. While it's not possible to use an <input>
element in <svg>
directly, the <foreignObject>
element allows HTML to be embedded in SVG.
It is possible to programmatically shift focus to an <a xlink:href="…">
element in Microsoft Edge 13+ by going through the following steps:
- append
<foreignObject><input /></foreignObject>
to the target element - call
element.focus()
on the input element - set the input element to
element.disabled = true;
- remove the
<foreignObject>
element injected in step 1 - the target element has received focus and become the active element
As of v1.4.0
the utility ally.element.focus
incorporates the above workarounds to focus SVG elements in all versions of Internet Explorer and Microsoft Edge.
# The blur SVG element hack
Browsers that don't provide the .focus()
method on SVGElement
also lack the .blur()
method. Sadly calling document.body.focus()
won't achieve what document.activeElement.blur()
should've done. But shifting focus back to <body>
is possible by going through the following steps:
- append
<input>
todocument.body
- call
element.focus()
on the input element - call
element.blur()
on the input element - remove the
<input>
element injected in step 1 document.body
is now the active element
As of v1.4.0
the utility ally.element.blur
incorporates the above workaround to shift focus away from SVG elements in all versions of Internet Explorer, Microsoft Edge and Firefox below version 51.
# Styling focused SVG elements
All browsers support the CSS pseudo-class :focus
. However, Firefox until version 50, Internet Explorer and Edge including version 15 do not support the CSS property outline
on SVG elements.
It is also worth noting that <a>
elements do not have dimensions of their own. The focus outline can therefore only be drawn on the element's bounding box, which is the smallest rectangle enclosing all of the <a>
element's children (demo). WebKit (Safari 10) does not render the focus outline on an <a>
element's bounding box, so the focus state of SVG links is not visually indicated. Internet Explorer and Edge render the native focus outline, as long as the outline
property isn't set.
These limitations make it necessary to indicate the focused element another way, e.g. by changing fill
or stroke
.
Using stroke
on text will create a different kind of outline than produced by the CSS properties outline
or border
on HTML elements. A rectangular outline similar to the browser's default focus outline would have to be rendered using SVG itself, e.g. by way of a <rect>
element.
Also note that browsers are inconsistent about how focus outlines should behave in scaled images, as the following table shows (test case):
Browser | Native focus indication | outline |
stroke |
---|---|---|---|
Firefox 51 | scaled | scaled | scaled |
Chrome 55 | scaled | scaled | scaled |
Safari 10 | fix | scaled | scaled |
MS Edge 15 | fix | not supported | scaled |
# The <foreignObject>
element
The <foreignObject>
allows arbitrary HTML to be embedded in SVG. This includes interactive elements like <input>
elements, as we've already seen in the focus SVG element hack. However, the presented hack (deliberately) omitted a few elements and attributes.
Most resources on the web, including the SVG 2 spec present the <foreignObject>
element in a setting like the following:
<svg xmlns="http://www.w3.org/2000/svg">
<switch>
<foreignObject requiredExtensions="http://example.com/SVGExtensions/EmbeddedXHTML">
<body xmlns="http://www.w3.org/1999/xhtml">
<p>HTML content</p>
</body>
</foreignObject>
</switch>
</svg>
It's not immediately obvious that the snippet won't render the text HTML content in any browser. The reason is the invalid extension URI http://example.com/SVGExtensions/EmbeddedXHTML
provided in the requiredExtensions
attribute. But even if the correct URI http://www.w3.org/1999/xhtml
is used, Chrome still won't render the embedded HTML. Similar problems exist with the requiredFeatures
attribute, which was dropped in SVG 2. See the following table and example for a support matrix:
embed type | Chrome | Firefox | Edge |
---|---|---|---|
invalid extension | not rendered | not rendered | not rendered |
valid extension | not rendered | rendered | rendered |
invalid feature | rendered | not rendered | not rendered |
valid feature | rendered | rendered | rendered |
no extension or feature | rendered | rendered | rendered |
play with Embedding HTML in SVG on jsbin.com or open the document of Embedding HTML in SVG
Do not use the attributes requiredExtensions
or requiredFeatures
on <foreignObject>
, as there is no JavaScript API to determine if a <foreignObject>
is being rendered because of these restrictions.
Note that the <foreignObject>
element is not supported in Internet Explorer 11 and below.
# The <defs>
element
The <defs>
element defines that descendants are not rendered directly and should therefore not be focusable according to SVG 2 - 15.9. Focus. But currently all browsers consider the link in the following snippet keyboard focusable:
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<g id="use-somewhere">
<a xlink:href="…"><text>a link</text></a>
</g>
</defs>
</svg>
Do not put interactive content in <defs>
to avoid visual focus from getting lost.
# The <use>
element
The <use>
element references other content, that is rendered in its stead. The <use>
element is a Shadow Host, hiding all of the referenced content.
If the referenced content has focusable elements and one of those shadowed elements has focus, the <use>
element will be the document's active element. Currently only Chrome and Safari 10 have functioning Shadow DOM implementations to handle this "correctly". However, Chrome is not able to prevent shadowed content from receiving keyboard focus when the host is removed from the document's tabbing order via <use tabindex="-1">
.
As with other Shadow Hosts, Firefox does not properly encapsulate focused elements. Instead of the <use>
element becoming the active element, the focusable content within the ShadowRoot are exposed as the document's active elements.
Chrome has an interesting bug (665121) regarding the rendering of focus indication within <use>
elements, as can be observed in the test case. The previous section already explained that elements in <defs>
receive focus when they shouldn't. But to make things worse, the supposedly cloned elements in the <use>
elements indicate focus when the original element in <defs>
has focus. But when the clones themselves receive focus, there is no indication.
Safari 8 really trips up when it encounters <use>
elements that reference focusable content. Once a <use>
element's content received focus, it is not possible to shift focus with Tab and Shift Tab anymore. Keyboard users are effectively stranded, as this demo shows.
There is a weird problem with <use>
elements in Safari 10 (but not in Safari 9 or the current WebKit nightly). When <use>
elements are not surrounded by whitespace (e.g. <svg><use xlink:href="…" /></svg>
), the document's tabbing order is truncated so that all elements following that <use>
are not accessible via the Tab key anymore, as this demo shows.
Do not reference interactive content in <use>
. Browser support is not great and in the case of Safari 8 a real problem.
# Hidden SVG with focusable content
We must be careful when hiding SVGs that host focusable content. Opening the demo in Safari will show how focus cannot be shifted from the first input to the second using the Tab key. The reason is this hidden (via display: none
or visibility: hidden
) SVG link in between. While Blink 586200 has been fixed in early 2016, WebKit 154114 is still alive and kicking in Safari 10.
# Recap
- use
ally.element.focus
andally.element.blur
to programmatically shift focus SVG elements. - Avoid the attributes
requiredExtensions
andrequiredFeatures
on<foreignObject>
, since their interpretations vary among browsers. - Add
tabindex="0"
to all interactive SVG content. - Visualize
:focus
state using styles other thanoutline
, as that CSS property is not widely supported for SVG elements. - Do not host focusable content in
<defs>
, as these elements are not made properly inert by browsers. - Do not reference focusable content in
<use>
, since Shadow DOM isn't yet properly implemented in most browsers. - Do not hide SVG content that hosts focusable elements, as that would break the ability to traverse the document's tabbing order via the keyboard in Safari.
Thanks to Amelia Bellamy-Royds for taking the time to review this piece and provide some very good comments and insights!