Skip to main content Accessibility Feedback

HTML as the baseline

On Wednesday, I wrote about how the web is mostly just links and forms.

Google Docs? A website. Accounting software like QuickBooks? A website. Discord and Slack? Websites. Apple Maps? A website.

A lot of folks I talked to were skeptical about that.

So today, I wanted talk about HTML as the baseline: what that looks like, how to build things that way, and maybe even why you’d want to.

Let’s dig in!

The approach

Here’s how a links-and-forms approach to web development works.

  1. If clicking a thing takes you somewhere else, use a link. When someone clicks it, navigate them to the new URL, which loads more HTML.
  2. If something is an action, use a button element wrapped in a form. When the user clicks it, the form submits to a server, which does some stuff, then redirects them somewhere or shows them a message.

This isn’t revolutionary. This is the backbone of the web!

My belief is that this is how all most websites and web apps should work by default. Then, we can use JavaScript to progressively enhance them into something more Ajaxy.

Implementation details

The high-level approach is the easy part. The implementation details are where it gets messy.

(If you want help with that, I offer consulting services, and have worked with some awesome organizations both big and small. I’d love to work with you!)

Some decisions that you’ll need to make along the way:

  1. Are you pre-rendering HTML using a static site generator (SSG), or generating HTML on the fly using server-side rendering (SSR)?
  2. If you’re using pre-rendered HTML, how do you deal with dynamic content? (I’ve found a hybrid approach that I think works quite nicely.)
  3. When a form is submitted, do you redirect to another page on success, or display some HTML and text dynamically?
  4. How do you show error messages when things go wrong?

The answers to these questions will vary based on what you’re trying to do and the skillset of your team.

I’ve found that for my personal team-of-one projects, using a static site generator to create mostly static HTML with a sprinkling of PHP lets me have the best of both worlds and works out really well for. I can generate dynamic error messages from query strings and show user-specific content without needing a whole SSR app setup.

For a bigger team or a team that’s more heavily JavaScript oriented, something like Astro and even vanilla Node might be a good choice.

Can you give an example?

Yep! The Lean Web Club works (almost) 100 percent without JavaScript.

Take something like the login form…

<form action="/path/to/login.php" method="post">
	<label for="username">Username</label>
	<input type="email" id="username" name="username" required>

	<label for="password">Password</label>
	<input type="password" id="password" name="password" required>

	<button>Login</button>
</form>

By default, when someone clicks the Login button, the form first validates the fields using native HTML form validation. If the required fields are left blank, or the [type="email"] field isn’t a valid email address, error messages are shown to the user.

If everything checks out, the form makes a POST request to the /path/to/login.php URL.

In my login.php file, I validate the username and password. If they check out, I do a server-side redirect to the user dashboard, where they can view courses and start learning.

But what about errors?

For those, I redirect the user back to the login page, with a query string that includes an error message.

https://leanwebclub.com/login?error=The+username+and+password+you+provided+are+not+correct.

Earlier, I mentioned that I use a hybrid SSG/SSR approach. I’m actually generating index.php files with my static site generator.

Inside the code for my login page, I actually check for an error query string parameter. If one exists, I show it in the form.

<form action="/path/to/login.php" method="post">
	<label for="username">Username</label>
	<input type="email" id="username" name="username" required>

	<label for="password">Password</label>
	<input type="password" id="password" name="password" required>

	<button>Login</button>

	<?php
		$error = $_GET['error'];
		if (!empty($error)) :
	?>
		<p class="error"><?php echo $error; ?></p>
	<?php endif; ?>
</form>

This lets the whole form work without JavaScript, and I can still surface meaningful information to the user.

I use PHP, of course, but this would work with any server-side tech.

Then you make it ajaxy

Next, I wrap my form in a generic Web Component I use to handle form submissions (this example omits the PHP for clarity and brevity.)

<ajax-form>
	<form action="/path/to/login.php" method="post">
		<label for="username">Username</label>
		<input type="email" id="username" name="username" required>

		<label for="password">Password</label>
		<input type="password" id="password" name="password" required>

		<button>Login</button>
	</form>
</ajax-form>

It intercepts submit events on the form. Then, it makes a fetch() request to the action URL using the defined method.

I include a special header to let my server know it’s an ajax request and not a full form submission.

With that header present, the server sends back a JSON object rather than doing a redirect. If the login was successful, the JSON object include a redirect URL to send the user to. If it wasn’t, it includes an error message to show in the UI.

Why do this?

I do some variation of this pattern through the whole app.

  1. An HTML form as the baseline, with a server handler.
  2. A Web Component to make it ajaxy when JavaScript is available.

It has dramatically reduced the complexity of the app.

Frankly, HTML is much easier to read and reason about. I’m not hunting around for various JavaScript files and chasing rabbits down nested module files anymore. The HTML tells you what it does, and if I need to dig into it, I open up the relevant action file.

Because it’s all forms doing the same thing, my lone <ajax-form> Web Component can handle a myriad of different use cases. I just need to make sure the server sends back predictable and consistent JSON responses.

This won’t really work for bigger organizations

Ah, but it already has!

For years this was how GitHub worked. An HTML baseline as the default experience. Enhance with JavaScript later (increasingly using Web Components).

Last year, one team started using React for one feature (Projects). Since they were already paying the “React tax,” it started infecting other projects. And the whole site has become slower and buggier as a result.