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

buttons 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 buttons 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 an inline-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 buttons

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 with display: contents will mostly resemble and word-wrap like body text. However, the button continues to retain default button font styling. Notably the button 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 the button.

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, buttons aren’t that difficult to style in regards to typical button styling (e.g. buttons look like buttons because they’re buttons).
  • Due to current browser implementations, buttons will always be display: inline-block unless you use display: 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 a button’s inline-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:

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.