Unbuttoning Buttons
Regardless of how one might feel about using CSS to disguise an HTML element as another, it can’t be denied that it’s a common, and sometimes necessary, practice in modern web development.
It’s often better to just use the HTML element you need, rather than modifying an element to look, act, and convey the same semantics as another. However, sometimes the necessary HTML element isn’t malleable enough for the task at hand.
For instance, let’s talk about styling (or rather unstyling) the button
element.
Trickier than you might think
button
s can be an interesting element to wrangle if “full styling control” is the end goal. For instance, if you’ve taken the time to poke around Normalize.css you might have noticed the number of rulesets in play to get button
s into a comparable cross-browser state.
Excerpts of normalize.css button
styles
/**
* Note: Rules have been combined to cut down on redundancy.
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
* 3. Show the overflow in Edge.
* 4. Remove the inheritance of text transform in Firefox.
* 5. Correct the inability to style clickable types in iOS and Safari.
*/
button {
-webkit-appearance: button; /* 5 */
font-family: inherit; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
overflow: visible; /* 3 */
text-transform: none; /* 4 */
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring {
outline: 1px dotted ButtonText;
}
In regards to implementing standard button designs, with the noted normalizations in place, styling should be fairly straightforward. Update the padding, borders, colors, hover and focus styles… go to town really.
However, normalizing won’t help if the goal is to make a button
not visually behave like a typical button
. Specifically, if you want a button to not look like a button, and its text to be treated as if it was an inline text element (such as a span
or a
element).
For some initial context as to why you can’t just slap a display: inline
on a button
selector and call it a day, take a gander at the HTML specification’s section on button
rendering.
The button
rendering section states:
The
button
element is expected to render as aninline-block
box rendered as a button whose contents are the contents of the element.
Essentially the browsers leaned hard into this section, and outside of a specific CSS display
value (more on that later), button
is going to continue to be rendered (or rather semi-render) as inline-block.
A bug or a feature?
Note: there has been some steps taken to address this issue. But the issue is more pertaining to allowing flex, grid and columnset layouts inside `button` elements, which doesn't actually help with fully negating the `inline-block` presentation of the element itself.
For instance, a `button` set to `display: block` will break to a new line, as if it was a block, however it won't go 100% width, as one would expect for an element with this `display`.
But this is web development. Where there is no path forward, we hack and slash our way through!
tldr; you’ll need some ARIA (and a bit of JavaScript) for this
My initial reaction to puzzles like these is to try to find an alternative native HTML (and/or CSS in this case) solution. As Matt Mastracci noted, in Micah’s twitter thread, a span
with an appropriate set role
and tabindex
could provide the solution to this issue.
<span role="button" tabindex="0" onClick="...">
...
</span>
Granted, a bit more JavaScript would be necessary to allow the “button” to be activated by use of the Space and Enter keys (which a native button
would have given us for free). But for all intents and purposes Matt was correct. This is the solution to the problem.
So why am I still writing?
Because we should exhaust other native possibilities before we jump to ARIA.
CSS walks into a bar. The bar is now an empty parking lot.
An alternate solution suggested for this problem was to use CSS’s display: contents
. And we need to talk about that a bit.
There was a lot of fanfare about display: contents
as it was getting implemented by browsers. Unfortunately, Hidde de Vries’s post More accessible markup with display: contents
calls out a troubling issue with browsers. It seems that when implementing, browsers did not meet the CSSWG specification’s requirements to not modify the semantics of elements modified by display: contents
.
From the specification:
Aside from the ‘none’ value …the
display
property only affects visual layout: its purpose is to allow designers freedom to change the layout behavior of an element without affecting the underlying document semantics.
Separately, Adrian Roselli also noted additional accessibility issues with using display: contents
, noting that it should not be considered a “CSS reset”.
Getting back to button
s
Picking up where those two left off, I ran a series of tests to determine if there was another way to use CSS to make a button
element behave as inline text, while checking for any accessibility issues. In regards to display: contents
, the following is a summary of my testing with the noted screen readers and browsers:
- Visually, a
button
styled withdisplay: contents
will mostly resemble and word-wrap like body text. However, thebutton
continues to retain defaultbutton
font styling. Notably thebutton
loses the ability to be keyboard focused. - No screen readers announced the element as a
button
. Screen readers used:- VoiceOver (Safari 12)
- TalkBack 6.2 (Android 8.1.0 + Android Chrome)
- VoiceOver (iOS 11.4.1 + Safari)
- JAWS 2018 (latest release)
- NVDA 2018.2.1
- Checking the accessibility tree, “no accessibility node”, or “text” was reported in each browser tested, where “button” should have been expected:
- macOS Safari 12 (using accessibility inspector)
- macOS Firefox 62.0.3 (using Firefox dev tools a11y panel)
- macOS Chrome (latest) (using Chrome dev tools a11y panel)
- iOS 11.4.1 Safari (using accessibility inspector)
- Windows 10 Firefox 64.0a1 (using aViewer)
- Windows 10 Chrome (latest) (using aViewer)
display: contents
is unsupported in Internet Explorer and Edge, so these browsers had no visual or semantic changes to thebutton
.
Note: You can view the full test results here.
It’s also worth noting, as Adrian does in his article, that even trying to reapply semantics with ARIA (e.g. <button role="button" style="display: contents;">
) is useless. The semantics are just gone.
An alternative to the alternative all: unset;
As time went on, Twitter user @Cos_Anca suggested using CSS’s all: unset;
. This idea was interesting to me as I’d all but forgotten about this property.
When giving the idea a shot, I was quick to think that it solved Micah’s issue, while also retaining the necessary keyboard functionality and semantics of the button
.
Updated unfortunately my previous tests about all: unset
were flawed and after having a conversation with Ilya Streltsyn it was revealed that a button
’s inline-block
is not removed when using all: unset
.
Even if all: unset
did solve the problem, it has the same support issues as display: contents
, in that Internet Explorer 11 and Edge don’t support it. So the fact still remains that even if it did solve the issue, all
doesn’t have full browser support to be a viable option.
So what did we actually learn here?
There was a lot going on in this post, so I’ll summarize a bit:
- With some normalization,
button
s aren’t that difficult to style in regards to typicalbutton
styling (e.g. buttons look like buttons because they’re buttons). - Due to current browser implementations,
button
s will always bedisplay: inline-block
unless you usedisplay: contents
, which will also presently destroy their semantics. all: unset
is like the nuclear option for styles. Typically you probably don’t want to use this, and unfortunately abutton
’sinline-block
is like a cockroach as it still survived the unset blast.- Not covered in this post, but visually hiding the button and using a
label
(which renders inline) as a stand-in, has its own set of complications. - When native options aren’t possible, including necessary legacy/modern browser support, ARIA may be the answer. And in regards to this specific problem, ARIA is currently the best option we have.
Just always remember ARIA is Spackle, Not Rebar. It might end up being the only tool at your disposal, but that doesn’t mean you shouldn’t look for the right tool first.
More Reading and Resources
For more information about the topics discussed in this post, I recommend reading the following:
- Display: Contents Is Not a CSS Reset (2018)
- More accessible markup with
display: contents
(2018) - Links vs. Buttons in Modern Web Applications (2016)
- Short note on what CSS display properties do to table semantics (2018)
- Get Ready for display: contents (2018)
- Browser support for
display: contents
- Browser support for CSS
all
- All or Nothing (2016)
Additional Credits
Ian Devlin and Sven Wolfermann for researching/reminders about the button
element rendering section of the HTML specification.
Ilya Streltsyn provided valuable feedback about this article and my flawed conclusions of all: unset
. The content pertaining to all
has been revised based on the corrected tests. Thank you again.