Skip to main content

Journal

Should I use a button or a link?

I’ve written previously about the important differences between buttons and links. While reviewing some “component refresh” design mocks at work yesterday I noticed the designs were a bit unclear in this regard so I sent the designers a little decision-tree, which I’m noting here for future reference.

It’s important both for our users and for us as practitioners to distinguish between links (the <a> element) and the <button> element. The reason I push this is because they’re fundamentally different functionally, which has important usability implications. Users expect to use mouse, keyboard, browser back-button and assistive tech differently for links than they do for <button>s. And if they can’t visually distinguish one from the other, they’ll try things they expect to work then get confused when they don’t work.

I think this is an area where design and materials can’t be considered separately and need a joined-up approach.

Here’s a flow I hope is helpful.

Ask: does it…

  1. take the user to another page? Then it’ll be a link – the <a> (anchor) element.
  2. cause something to change on the current page, or submit a form? Then it’ll be a button – i.e. the <button> element.

If it’s a link (<a>):

  • it should be underlined so people know it’s a link
  • it should have a hover state, for example stay underlined but change colour
  • in cases where it’s a CTA you might choose to design it to look button-like and remove some standard link affordances. Just be aware you’re only “calling” it a button. In real user-experienced terms, it’s still a link.
  • it does not natively have a disabled state. We shouldn’t be disabling links.

If it’s a button (<button>):

  • it should look like a button, i.e. like a pill or rectangle
  • It should not look like a link – that’d confuse users into thinking it takes them to another page.
  • So it shouldn’t be underlined by default or on hover. It should have some other hover state.

Testing the decision tree

Let’s take the example of a control for launching a modal dialogue.

The obvious choice is a button, because the control causes something to change on the current page. In this case it causes a dialogue to appear on the current page.

Some might argue that it could be a link. This is usually influenced by the fact that dialogues are often (perhaps inadvisably) used as a kind of “fake page”. And to get someone to a “page” we use a link, right? Advocates of the link option might also have progressive enhancement in mind. If they present a link either to a named fragment further down the page or to a separate page, that offers a resilient baseline experience regardless of whether or not JavaScript is available. The idea is that they also have JavaScript to enhance the link when the user’s environment supports it, perhaps adding role=button.

However a button is the more accessible and user-friendly approach for launching the modal.

Better accessible names | hidde.blog

Accessible names are used by assistive technologies to refer to elements on a web page. Hidde tells us how to word them so that they are more useful.

Because of how we use accessible names, we want to keep them functional and avoid naming controls after what they look like. Ideally, you do this in the imperative form, that makes it easiest to quickly grasp what a thing is going to do.

My summary of Hidde’s top tips is:

  • Name after function rather than form (“Open navigation” over “Hamburger”)
  • Put the most unique part first as this makes it more easy to distinguish between elements (so “Midnight Marauders - Album” rather than “Album - Midnight Marauders”)
  • Be concise – keep a name to the most important 1-3 words
  • No roles as part of the name (for example don’t include the word ‘button’ or nav. The browser already handles this part so it’d be redundant and annoying)
  • Start names with a capital letter and don't end with a full stop, because a name is not a sentence
  • Avoid ARIA if you can just use good element text content

Using the :has pseudo-class for real

By day I’m currently working on our Design System’s Table component. In order to achieve a design spec where the table has no bottom-border I needed to set:

  1. all cells in the final row of the <tfoot> to have no bottom-border; but also
  2. if there is no <tfoot> then set all cells in the final row of the <tbody> to have no bottom-border.

Modern CSS’s support for writing selectors which traverse the DOM up, down and sideways is pretty amazing here.

I’ve gone with:

tfoot > :last-child td, tbody:not(:has(+ tfoot)) > :last-child td { border-bottom-width: 0; }

(Some BEM stuff renamed for brevity but that’s the gist of it)

In the past we’ve had to bloat the backend layer with complex and awkward logic that adds “convenience classes” like .fe-Table-bodyLastRow but as Eric Meyer has been saying :has() in particular is going to remove the need for those convenience classes.

Hat-tip to Jhey Tompkins for his excellent recent article on :has which was a great help.

How to Fix Common iOS Accessibility Issues | Deque

Although I don’t work on native apps, I’ve recently been wondering about how accessibility considerations for them compare to those for websites. So this is a timely and useful reference.

iOS provides a lot of accessibility behavior for free, so it’s a great start to making a mobile application accessible. Unfortunately, accessibility is more complicated than the iOS behavior can address, and using only default behavior can actually cause the app to have additional issues.


Tabs: truth, fiction and practical measures

My colleague Anda and I just had a good conversation about tabs, and specifically the company’s tabs component. I’ve mentioned before that our tabs are unconventional and potentially confusing, and Anda was interested to hear more.

What’s the purpose of a tabbed interface?

A tabbed interface is a space-saving tool for collapsing parallel content into panels, with one panel visible at a time but all accessible on-demand. While switching between tab panels the user is kept within the same wider context i.e. the same page, rather than being moved around.

Conventional tabbed interfaces

Here are some great examples of tabs components.

Tabs are a device intended to improve content density. They should deliver a same-page experience. Activating a tab reveals its corresponding tab panel. Ideally the approach employs progressive enhancement, starting as a basic Table of Contents. There’s quite a lot of advanced semantics, state and interactivity under the hood.

Faux tabs

But in our Design System at work, ours are currently just the “tabs” with no tab panels, and each “tab” generally points to another page rather than somewhere on the same page. In other words it’s a navigation menu made to look like a tabbed interface.

I’m not happy with this from an affordance point of view. Naming and presenting something as one thing but then having it function differently leads to usability problems and communication breakdowns. As the Inclusive Components Tabbed Interfaces page says:

making the set of links in site navigation appear like a set of tabs is deceptive: A user should expect the keyboard behaviors of a tabbed interface, as well as focus remaining on a tab in the current page. A link pointing to a different page will load that page and move focus to its document (body) element.

Confused language causes problems

One real-life problem with our tabs is that they have been engineered as if they are conventional tabs, however since the actual use case is often navigation the semantics are inappropriate.

We currently give each “tab” the ARIA tab role, defined as follows:

The ARIA tab role indicates an interactive element inside a tablist that, when activated, displays its associated tabpanel.

But our tabs have no corresponding tabpanel; they don’t use JavaScript for a single-page experience balancing semantics, interactivity and state as is conventional. They’re just navigation links. And this mismatch of tabs-oriented ARIA within a non-tabs use case will do more harm than good. It’s an accessibility fail.

A stop-gap solution

If content for one or more tabpanel is provided, apply the complicated ARIA attributes for proper tabs. If not, don’t. This means we allow component consumers to either create i) a real tabbed interface, or ii) “a nav menu that looks like tabs” (but without any inappropriate ARIA attributes). I don’t agree with the latter as a design approach, but that’s a conversation for another day!

Tabs in the future

Some clever people involved with Open UI are using web components to explore how a useful tabs element could work if it were an HTML element. Check out the Tabvengers’ spicy-sections component. Again, this is based on the conventional expectation of tabs as a same-page experience for arranging content, rather than as a navigation menu. And I think it’d make sense to stay on the same path as the rest of the web.

Editable table cells

Yesterday the Design System team received a tentative enquiry regarding making table cells editable. I’m not yet sure whether or not this is a good idea – experience and spidey sense tell me it’s not – but regardless I decided to start exploring so as to base my answer on facts and avoid being overly cautious.

In my mind’s eye, there are two ways to achieve this:

  1. on clicking the cell, the cell content is presented in an (editable) form input; or
  2. apply the contenteditable attribute

In both cases you get into slightly gnarlier territory when you start considering the need for a submit button and how to position it.

I don’t have anything further to add at the moment other than to say that if I had to spike this out, I’d probably start by following Scott O’Hara’s article Using JavaScript & contenteditable.

I’d probably also tweet Scott and ask if he can say anything more on his closing statement which was:

I have more thoughts on the accessibility of contenteditable elements, but that will also have to be a topic for another day…

Update 27-09-22: I’ve also remembered that (if I were to pursue Option 1: input within cell) Adrian Roselli has an article on Accessibly including inputs in tables.

Building the main navigation for a website (on web.dev)

learn about semantic HTML, accessibility, and how using ARIA attributes can sometimes do more harm than good.

Alongside all the sound accessibility and hiding-related advice, I also found Manuel’s approach to progressive enhancement interesting. Rather than i) include a hamburger button directly in the DOM and set its initial state to hidden; or ii) create the button element with JavaScript, he instead nests the button in a template element then clones that element with JavaScript. He later tweeted his rationale for this approach:

If JS doesn't work, the markup inside the template won't be rendered on screen and it's more convenient to prepare the markup upfront instead of using document.createElement().

The ability to prepare complex, JS-dependent component markup upfront in declarative HTML rather than recreating it in JavaScript is a compelling argument for his approach. Especially so if you don’t work in JS framework-based systems therefore your components are not written in JavaScript.

Brilliant first day at We Out Here 2022

Had such a good day yesterday @weoutherefest with Tom, Jason and Craig. In the afternoon we checked Enny, Sally Rodgers (A Man Called Adam) and Aletha. We then stopped for a short break to sample the food and drink (Char Sui Vermicelli from the NAM stall and a break from beer for a cracking coffee). Our nighttime choices were Bake, Charlie Dark, Pearson Sound and Alex Nut.

The absolute highlight was Charlie Dark in the forest. Amazing woodland setting and a killer set! Charlie has such great energy both on the decks and on the mic. He played a mix of house, techno, disco and broken biznizz – right up my street! Great mixing too (aided by his old-school lollipop headphone).

Time for Round Two!

Use CSS :has to set root-level styles based on a button’s state

Great tip here from Jhey. He advises using a button with ARIA and a little JavaScript for your dark-mode toggle. And to apply the dark styles, use a CSS selector which targets the :root parent of the button when in “pressed” state and sets a root-level custom property to “on”.

:root:has([aria-pressed=true]) {
  --dark:1;
}

Seriously clever stuff!

And aside from the CSS, I really like the way Jhey advocates using a button rather than a form element such as a checkbox for this kind of interface, much like Léonie did recently.

Experimenting with CSS cascade layers

Back in June I attended CSS Day in Amsterdam. One of my favourite talks was The CSS Cascade – A Deep Dive by Bramus van Damme. Bramus covered everything we wanted to know about the cascade but were afraid to ask! And that included an introduction to CSS Cascade Layers – the latest game-changing CSS feature.

I previously enjoyed Stephanie Eckles’s article Getting Started with CSS Cascade Layers so my interest was already piqued. However seeing Bramus’s talk in person really helped bring home the practical benefits of CSS layers.

For example, currently if we have the selectors ul[class] defined early in the “reset” section of our styles and .nav defined later in a “components” section, the former selector wins due to its higher specificity. That’s not what we want – we want our component styles to override our global styles. This has previously led people to hack around the problem by adding extra specificity to the latter selector, or by wrapping :where() around the former to decrease its specificity, neither of which are desirable. With layers we can do:

@layer reset, components;
  
@layer reset {
  ul[class] {
    /* set margin to 0 here */
  }
}
  
@layer components {
  .nav {
    /* give .nav ul custom margins */
  }
}

And .nav gets that higher specificity that we want.

I also like that layers align well with an ITCSS approach.

Taking layers for a spin

Cascade Layers are relatively newly-supported in browsers but pretty all-or-nothing in CSS terms to the extent that using them in a progressively-enhanced approach isn’t really an option. So I wouldn’t yet advocate using Layers on an important production website. However there’s nothing to stop me test-driving them on my personal site. Users on old browsers still get the essential content (although very few styles) and for this site, that’s good enough. So I’ve taken the plunge.

And it’s working really well already! Recently while rewriting some CSS I found that if the top of my styles featured a reset which disabled margins on h2 but I also wanted an h2 to take on margins when used within a low-specificity flow utility, the reset style was winning and that was frustrating. But when I moved my reset styles into a reset layer and my layout styles into a layouts layer with the layer order set as reset, layouts then the layout styles win! Really cool.