A List Apart

Menu

Illustration by

Let Links Be Links

The concept of the web as an application platform has never been more popular, but the tools used to create these so-called “web apps” are still fraught with pitfalls that are often ignored or misunderstood. Single-page web app frameworks have gained traction because they can easily be used to create fast, complex applications that feel much more solid and interactive than traditional websites. But this benefit, and the changes in mindset and development practices that accompany it, comes at the cost of basic browser functionality that web developers sometimes take for granted.

JavaScript can be fragile

Article Continues Below

With vendors making it increasingly difficult to disable, we can get lulled into thinking that we don’t need to provide a fallback for users whose browsers don’t execute JavaScript. But explicitly choosing to disable JavaScript is far from the only reason a user’s browser might not run it. Government Digital Service (GDS), the team that maintains the UK government website, found that, out of every 500 visitors to GOV.UK, five did not receive JavaScript, but only one had JavaScript explicitly disabled. The other four could be missing out on JavaScript for any of several reasons: an overzealous corporate proxy server; a request for JavaScript timing out due to high latency; or even an unnoticed syntax error.

Furthermore, JavaScript—unlike CSS and HTML—does not degrade gracefully. This means that if developers use a single ES6 syntax feature, or even make a single standard library function call without checking that the function has been defined first, their JavaScript could either stop running midway through execution or not run at all. When JavaScript is used to enhance websites, this doesn’t matter so much—visitors can still follow links and submit forms and generally use the web as intended. But when JavaScript is a requirement, anyone using even a slightly older browser is likely to get a blank page—and no explanation of what to do about it.

Semantic structure is still important

As designed by Tim Berners-Lee in 1993, HTML defined a common structure for the mesh of interconnected documents we now know as the web. The semantic meanings imbued in this common structure provide machine-readable context for the information contained in a web page. In practical terms, this extra information enables web browsers to enhance the user experience. For example, a web browser can implement a way to add events defined with time elements to a user’s calendar; a screen reader can read through a list differently than it would a paragraph. The difference between a list and a paragraph is clear to human viewers of a document; the common structure provided by HTML makes it clear to computers, too.

The semantic meaning behind HTML sets the web apart from native application environments like Cocoa, Windows Presentation Foundation, and Qt. Structured information matters more to the web because of the diverse ways in which it can be accessed. When I create an iPhone application, I can safely assume that every person using that application will use it in a similar way. The information my app presents will always be presented in much the same way, and I will always have complete control over that presentation. Even if someone using my app interacts with it through VoiceOver (Apple’s assistive technology for people with vision impairments), they still interact with the application in a similar way to a sighted user: by tapping around on the screen. They just happen to be hearing the text instead of seeing it.

The web doesn’t work that way. Websites aren’t viewed solely through web browsers. People consume websites through apps like Pocket or Instapaper, which try to use the structured information of a web page to extract its relevant content. A browser on a smartwatch might ignore your layout and present your information in a way that’s more suitable for a one-inch screen. Or—who knows?—your website might be used through some future device that will transform the information into thoughts beamed directly into a user’s brain. Even web screen readers don’t work like VoiceOver does on an iPhone, reading out the text in the order it’s laid out under a user’s finger. Web screen readers read through the whole document, ignoring layout, and infer meaning from the standardized semantic definitions of HTML tags. A simple example of when semantics like this matter is the recently introduced main element, used to define the main part of a document. To a sighted user viewing your website through Google Chrome, whether you use <main> or <div id="main"> makes no difference. To someone using another web client, though, such as a screen reader or Instapaper, the meaning implied by the main element is very important to their software in helping them navigate your document.

Developing an application for the web, therefore, is not as simple as developing for a native platform. Making sure it works the way we want it to in the five major browsers and pushing it out isn’t good enough for the web. We need to test our work in screen readers. We need to review our markup to make sure it provides as much semantic metadata as possible—not just for today’s web clients, but for tomorrow’s as well.

Single-page web app frameworks

When using “single-page web app” frameworks like Angular and Ember, the trend is to treat websites like native apps, with little regard for the nuances that make the web unique. Developers make assumptions about their users that can seriously damage the experience of people who don’t meet those assumptions. As an example of what this mindset can result in, consider the markup for a login button (since changed) that I recently found on Patreon’s site:

<span class="patreon-button sub-section navigation-active" data-ng-click="triggerChangeWindow(navigation.login_url)">Log In</span>
Example of a login button that once appeared on Patreon’s site
Patreon’s fairly standard login button acts just like a link. No need for special JavaScript here.

This link works fine for me in Safari, but in any environment other than a current mainstream browser, this button is totally useless. Let’s say we have a hypothetical smartwatch browser called WatchBrowse. Maybe it displays a list of links for the user to navigate through because this particular smartwatch doesn’t have a cursor that can interact with the page. Because HTML defines a standard way to create links on a web page (the a element), WatchBrowse could theoretically just list every a tag on the page with its href attribute and content—until a site like Patreon comes along and decides to shun web standards in favor of reimplementing basic browser functionality from scratch.

If Patreon had used an a tag instead of a span, WatchBrowse could perhaps find the link and display it in the list. When a user selected the link, it could simulate a click event instead of just using the href attribute. But what about functionality that requires the browser to know where the link is going to lead ahead of time? A browser extension might allow you to search links on a page by their href values, which would be useful if you wanted to quickly find where somebody links to their Twitter account, for example. Firefox shows where a link is going to take me when I hover over it. When link href attributes are no longer static values but are, instead, decided by arbitrary JavaScript in click handlers, these helpful features are no longer possible.

Patreon’s site is built with Angular, and while Angular is not at fault here per se, the mentality of treating HTML as a view layer that goes along with using these frameworks probably contributed to Patreon’s poor decision.

What if we created the same link the way the framework developers recommend in their documentation? A more standard way to make a link in Angular might look like this:

<a ng-href="/login">Log In</a>

When rendered into the DOM by client-side JavaScript, that snippet turns into this:

<a ng-href="/login" class="ng-binding" href="/login">Log In</a>

Ember handles this similarly. A link is defined in an Ember template like so:

{{#link-to sessions.new}}Log In{{/link-to}}

And when it’s rendered into the DOM, it becomes this:

<a id="ember-563" class="ember-view" href="/sessions/new">Log In</a>

Ember and Angular then intercept the link’s click event to render the new content without reloading the page. Crucially, though, if the click event were never fired and the browser loaded the value of href, there would be no visible difference to the user other than an extra page reload, because Ember and Angular by default don’t try to reinvent the wheel by defining their routing in terms of URLs.

In their current forms, however, Ember and Angular still require JavaScript to render their templates and create those links in the first place. Four out of every 500 people who visit a website built with Angular or Ember will encounter a completely blank page.

A solution?

When dynamic web page content is rendered by a server, rendering code only has to be able to run on that one server. When it’s rendered on a client, the code now has to work with every client that could possibly visit the website. Developers are now moving away from server-rendered websites because they don’t offer the sort of rich application experience that client-rendered sites can provide. But I think there’s still a role for server rendering in the new world of client-side applications.

At the moment, requiring JavaScript is a tradeoff that developers using single-page web app frameworks have to make, but it seems to me that this is exactly the sort of problem that should be handled by a framework. We are fortunate as web developers in that we write application code for the web in one of the most universal programming languages that has ever existed. If framework developers could put in the effort (which, admittedly, seems large) to get apps running in Node just as they run in the browser, initial page rendering could be handled by the server, with all subsequent activity handled by the browser. Crucially, if a server can render links into a tags, like Ember currently does on the client, it would be possible for a user who did not receive JavaScript (for whatever reason) to navigate around the website. It might be possible to get forms working as well, by running all the validation and submission logic on the server instead of on the client. If this effort could be made at the outset by a framework maintainer, then every developer using that framework could immediately transform an app that only worked on the latest web browsers into a progressively enhanced experience compatible with virtually any web client—past, present, or future.

Progressive enhancement has been important to web developers for a while now. It recognizes that the vital part of the web experience is content, and that any additional improvement to the user experience should not undermine the universal availability of content provided by the web. Current approaches to single-page web apps tend to abandon this principle, but progressive enhancement and single-page web apps are not fundamentally incompatible.

In fact, progress is being made in this arena. To improve Ember’s compatibility with search engines, for example, an in-house team is working on implementing server-side rendering. But the solution to the problems caused by single-page web apps is not purely technical: there’s a growing problem in the way people think about the web. It has become commonplace to treat the web as just another application platform—but the web is so much more than that. The web is the universal information platform. It doesn’t matter if somebody visits a website on a $2,000 iMac, or a $50 Android tablet, or the $5 web client of a future we can’t even imagine yet—in theory, at least. In practice, it’s important to make sure that we don’t sacrifice the experiences of a small number of users just so we can slightly improve the experiences of the rest, damaging the universal nature of the web in the process.

About the Author

23 Reader Comments

Load Comments