Select The Right Tool For The Job

If you’re using a static file server to serve an HTML page which implements a <select> to provide user navigation, you cannot achieve path-based navigation without 1) client-side JavaScript, or 2) server-side redirect logic.

In other words, path-based navigation with a <select> element will break for users where JavaScript fails — fails to load due to a network issue or a user preference, fails to parse, fails to execute, etc. — unless you control the server and can redirect a path with query parameters (/path?foo=bar) to a pure path (/path/bar).

The Problem

Many sites use a <select> element to afford user navigation.

It’s a tidy, spacing-saving solution that collapses a large number of choices into a single UI element, providing navigational options at the time they’re needed.

As a developer, an example of this you might see a lot is the “docs version switcher”.

Example of a website header navigation bar that shows the logo “Docs Website” and right next to it a select element with a semver version of 4.17.15

The neat thing about a <select> element is that, when clicked, it triggers a menu drawn by the underlying operating system in a manner best suited (and accessible) to the given user’s device and preferences.

In the case of a “docs version switcher”, you interact with the <select> and it gives you a menu of choices. Once you choose an option, it navigates you to your selected version of the docs.

Example of a website header navigation bar that shows the logo “Docs Website” and right next to it a select element that is open and showing a menu of different semver options

A common way to create this would be to write the <select> markup and add a JavaScript handler that redirects the user to the selected version of the docs.

<select onChange={(e) => {
  e.preventDefault();
  const version = e.target.value;
  window.location = "/docs/" + (version === "main" ? "" : version);
}}>
  <option value="main">Latest</option>
  <option value="6.3.0">6.3.0</option>
  <option value="5.x">5.x</option>
</select>

However, without JavaScript that onChange handler does absolutely nothing.

So, if you’re aiming for a progressively-enhanced experience, how do you use <select>?

Making Things Work Before (Or Without) JavaScript

By default (and before/without JavaScript), the <select> element won’t submit anywhere on its own. For that, it needs a <form>. It also won’t submit when changed. For that it needs a <button>. Combined, these constitute the semantics in HTML for declaratively describing the navigation a form will make.

<form action="/docs" method="GET">
  <select name="version">
    <option value="main">Latest</option>
    <option value="6.3.0">6.3.0</option>
    <option value="5.x">5.x</option>
  </select>
  <button type="submit">Switch</button>
</form>

When this form submits, it will do a GET to the action with the form elements parameterized, e.g. /docs?version=6.3.0

Now we have markup properly suited to the task of progressively-enhanced navigation (sprinkle some JS in there and you can hide the submit button, that way if/when JS executes the form submits onChange and the user is unencumbered by the presence of an unnecessary submit button).

But will this experience work before (or without) JavaScript? Maybe. There’s another caveat.

Are You Using a Static File Server?

If you’re doing static site generation, i.e. creating static HTML files on disk that map to URLs, you’ve got another obstacle to using <select>: routing.

In the “docs switcher” example, a standard SSG approach would mean architecting your site to generate different versions of the docs within different, nested folders on the file system whose structure will parity your public-facing URLs.

For example, your file structure is:

docs/
├── index.html
├── foo/
│   └── ...
├── bar/
│   └── ...
└── 6.3.0/
│   ├── index.html
│   ├── foo/
│   │   └── ...
│   ├── bar/
│   │   └── ...
└── 5.x/
    ├── index.html
    └── old-foo/

This would result in the following URLs:

However, as noted, by default the browser uses <select> to create a parameterized request. And for a static file server, a request to the same path but with different query parameters will result in the same response, e.g. a request for /docs?version=5.3.0 will result in the same response as a request for /docs?version=3.2.0.

Routing based on query parameters is impossible for SSG without assuming 1) working JavaScript on the client, or 2) control over a (host-specific) request/response mechanism on the server.

This presents a problem for you: you crafted a nice solution built on the native, semantic primitives of the web — the <form>, <select>, and <button> elements — but now you have a crisis around your site’s architecture because of how it’s built and hosted.

Now you have to consider your options. Here are a few:

Working With The Materials of The Web

Probably more than you ever wanted to know about <select>, I know.

But it’s intended to illustrate how interconnected our decision making can be when creating a website. If you’re planning on providing a universally-accessible, progressively-enhanced experience, then which HTML element you use can have ramifications for your choice of server and host.

Using <select> instead of <a> has its ramifications and tradeoffs (both on you as a developer and on your users). In the case of a <select> element, it visually collapses a large number of options into a single point of interaction whose UI is (in large part) under the control of the operating system. But functionally it is a <form> request which works by parameterizing the request URL.

A form like this:

<form action="/docs" method="GET">
  <select name="version">
    <option value="main">Latest</option>
    <option value="6.3.0">6.3.0</option>
    <option value="5.x">5.x</option>
  </select>
  <button type="submit">Switch</button>
</form>

Is a navigation on the web. Navigationally, it is similar to a set of links like this:

<a href="/docs?version=main">Latest</a>
<a href="/docs?version=6.3.0">6.3.0</a>
<a href="/docs?version=5.x">5.x</a>

A link gives you, the developer, more declarative control over navigation (you can easily change the href from query parameters to paths, for example). But it’s also a different experience for the end user. A <select> hides navigation options behind a menu while a set of links is revealed all at once.

You could, for example, try to mimic hiding a set of options behind a user interaction using something like <details>:

<details>
  <summary>Version: 6.3.0</summary>
  <ul>
    <li><a href="/docs/main">Latest</a></li>
    <li><a href="/docs/6.3.0">6.3.0</a></li>
    <li><a href="/docs/5.x">5.x</a></li>
  </ul>
</details>

But it’s still not equivalent. For example, <select> triggers a menu drawn by the respective OS that’s hard to style while <details> triggers an unfolding menu that’s more amenable to styling.

The point is, when considering a progressively-enhanced experience that will be accessible to the widest range of users, your technology choices are interconnected and opinionated. Some play nice with each other, others do not.

Conclusion

Choosing technologies that align themselves with the grain of the web as well as its underlying principles provides you the option to scale your solution up or down in complexity while preserving choice. You choose the web primitives that best fit your problem at hand — e.g. an <a> element or a combination of the <form>, <select>, and <button> elements — and bypass the need for custom escape hatches tailored to individual technologies.

Web-aligned technologies encourage you to ask, “How can I make this work best for my users?” This stands in contrast to technologies whose opinions burden you to ask, “How can I make this work at all?”

It’s a good bet to invest in tools aligned with the way the web is designed to work.