Progressive Enhancement

css html permacomputing

Why write a Hugo theme from scratch? One reason is to ensure our websites are designed with progressive enhancement in mind. In this post we explain the relevance of progressive enhancement to the climate crisis, detail exactly what progressive enhancement is, and then demonstrate how we use it in this Hugo theme. By refining the technology our websites are built upon, and documenting what we learn, we hope to initiate more conversations about how technology can support climate justice, and perhaps advance the state of the art in sustainable web design.

Table of contents

Climate justice means supporting older, simpler technology

One aspect of sustainable design is making websites (and other media + software) that can easily be used by people with older computers + technology. We cannot continue manufacturing computers and then rapidly throwing them away because of planned obsolescence. Do you really need the latest laptop? If you do, is it inherent to the work you do, or is it because the tools and websites you use are bloated and careless with your computer resources? We must move towards frugal computing and permacomputing (a parallel to permaculture), to make sure that computation and civilization itself survive.

Supporting old hardware and software, extending their useful lifespans to reduce electronic waste, while helping people with limited economic means, is a vital aspect of climate justice. Organizations like Free Geek work to provide equitable access to technology by refurbishing computers, but their efforts are hampered by new software arbitrarily requiring new hardware.

As always, the responsibility for supporting older/simpler technology should not be placed on individuals, our whole society must change. We hope that by releasing software (like this Hugo theme) that is designed to support old hardware, we can make permacomputing easier, and serve as an example that can be replicated by institutions who can change the web at significant scales, such as governments. Permacomputing practices should include progressive enhancement.

Progressive enhancement is one way to support old tech

Progressive enhancement is a way to build technical credit: the opposite of technical debt, taking steps to make maintenance easier in the future, and planning for the long term.

Progressive enhancement means designing media/software/websites using the simplest possible technology, so that anyone can access a basic version, and then enhancing the experience for people with more “advanced” (newer, or more complex and expensive) technology. In other words, people with older/cheaper computers or simpler software will see a version of our websites that is comfortable for them, while people with fancier technology will enjoy all of the bells and whistles to which they’ve become accustomed.

Crucially, separate is not equal. Progressive enhancement means showing different aspects of the same website to different people, it does not mean building different web designs where the less popular design can be neglected and left to rot.

Progressive enhancement can also be distinguished from “graceful degradation,” where you assume a certain level of advanced functionality / features, and provide fallbacks for when those are not available. This approach to design generally will not work so well for those without the desired features, and should be avoided unless your product absolutely requires those advanced features. We reject such excessively complex designs whenever possible.

How this theme uses progressive enhancement

In the README for this theme, we claim to have designed for progressive enhancement, supporting even old and obscure web browsers. How do we do this?

It’s hard to support software that you cannot use and test yourself. Therefore, we use web browsers like NetSurf, which runs on minimal hardware but does not support modern web standards like CSS3 and HTML5, as representatives of older/simpler browsers. We are able to install, test, and support NetSurf (which is still maintained and relatively safe to use), while we cannot test every wacky browser that ships in your television or refrigerator (and it might be a security risk if we did). Similarly, the Pale Moon web browser is a fork of an old version of Firefox, which has more features than Netsurf, but still lacks many modern browser features. (Although it does have a few features Firefox has lost over time, like a nice user interface for RSS feeds.)

Here are some strategies we use to support these less advanced browsers.

Doing nothing

Frequently, more advanced features are totally optional, and it’s fine to just let simpler browsers quietly ignore your code. For example, here’s how we enable dark mode on modern/evergreen browsers:

/* CSS Variables */
:root {
	--background: white;
	--color: black;
}

/* Dark mode colors */
@media (prefers-color-scheme: dark) {
	:root {
		--background: black;
		--color: snow;
	}
}

body {
	background-color: var(--background);
	color: var(--color);
}

This code sets the background to white and the text to black by default, and if your browser is set to dark mode, it instead sets the background to black and the text to an off-white called “snow”.

What happens if a browser doesn’t support CSS variables? The color declarations do nothing, and the browser will just use its default colors for the background and text. Theoretically this could be bad if your browser’s default colors are bad, but we trust you to set your defaults the way you want them. In other words, we don’t need to make any changes to our code, we just need to be OK with it not doing anything sometimes. (Crucially, you have to test to make sure that nothing bad happens when a block of code is nonfunctional, don’t assume it works the way you think it does.)

Duplicate declarations

In many cases, support for simpler browsers is as simple as repeating yourself.

CSS

In CSS, if you declare something first in simple terms, and then write it again using more advanced features, more advanced browsers will use the last declaration, while simpler browsers without those features will ignore the code they don’t understand, falling back to the simpler earlier declaration. Take for example this code:

/* CSS Variables */
:root {
	--pad: clamp(.5em, 2vw, 1em);
}

table {
	padding: 0 1rem;
	padding: 0 var(--pad);
}

This gives tables a little horizontal space so that they don’t hit the edges of the screen. On more advanced browsers, the amount of space changes based on the width of the “viewport” or browser window.

The more advanced code uses both CSS variables and clamp(), but any browser confused by CSS variables won’t even try to use clamp(). Simpler browsers will ignore the second padding declaration and simply add 1rem of horizontal padding, as declared in the previous line.

HTML

HTML works similarly. Observe these <link> tags in the invisible <head> section of our webpage:

<link rel=icon type=image/svg+xml href=/images/favicon.svg>
<link rel="alternate icon" type=image/png sizes=any href=/images/favicon-16x16.png>

This sets our favicon.

We prefer SVG vector graphics because they look great at any image resolution or screen size, you only need one file for many roles, and they tend to have much smaller file sizes. In simpler browsers, we can fall back to a tiny PNG raster image that may not look great at higher resolutions, but will still function to distinguish this website from websites in other tabs or bookmarks. MDN explains favicon priority:

If there are multiple <link rel="icon">s, the browser uses their media attribute, type, and sizes attributes to select the most appropriate icon. If several icons are equally appropriate, the last one is used.

Here we use sizes=any to mark the PNG favicon as lower priority, otherwise Chrome may download both the SVG and PNG versions unnecessarily.

For a more complex example, study this <picture> tag:

<picture>
	<source
		type=image/webp
		sizes="(min-width: 45em) 45em, (max-width: 45em) 100vw"
		srcset="/blog/2020/10/pictures/providence_540.webp 540w,
	/blog/2020/10/pictures/providence_720.webp 720w">
	<img
		sizes="(min-width: 45em) 45em, (max-width: 45em) 100vw"
		srcset="/blog/2020/10/pictures/providence_540.jpg 540w,
	/blog/2020/10/pictures/providence_720.jpg 720w"
		src=/blog/2020/10/pictures/providence_1440.jpg
		width=3036
		height=4048
		alt="Emily addresses Sunrise Movement activists from the stairs inside the Rhode Island State House rotunda.">
</picture>

The picture tag works a little differently, the browser will check the image formats from top to bottom and use the first one that it supports (rather than the last one as in CSS). Here we prefer webp images because they (usually) have smaller file sizes for the same image quality. We also would like the browser to choose the best image size for its screen resolution + pixel density, or in other words we want responsive images.

Browsers will try the webp images as listed in the <source> tag, and then they will try the jpg images of various sizes listed in the <img> tag’s srcset attribute. Finally, if the browser does not support srcset, it will fall back to the single image named in src.

Testing for CSS features with @supports

One tool we use to enhance our webpage for browsers with more advanced features is the @supports feature query, which lets us give instructions only to browsers that support specific features. @supports is only necessary in more complex circumstances where doing nothing or using a straightforward fallback is not good enough.

Here’s one example:

body {
	margin: auto;
	max-width: 45em;
}

img {
	max-width: 100vw; /* WebKit: don't let images get wider than viewport */
	width: 100%;
}

@media (min-width: 35em) {
	/* full-width images in modern/evergreen browsers */
	@supports (margin: 0 calc(50% - 50vw)) {
		.full-width {
			margin: 0 calc(50% - 50vw);
			width: 100vw;
		}
	}
}

Under the img selector, we declare that images should take up 100% of the width of the container, which will be the body area where all of the text is (limited to 45em in width). This applies to all browsers at smaller widths, e.g. smartphones, and continues to apply for simpler browsers at wider widths, when the body content is centered in the middle of the screen and doesn’t occupy the whole viewport. Simpler browsers will show a smaller image appropriate for the width of the body container.

For more advanced browsers, we want “full-width” images to break out of the body container and continue to occupy the entire width of the screen, even at larger screen sizes. In other words, we want more advanced browsers to use 100vw as the width, which sizes the image to fit the viewport / browser window, and simpler browsers to use 100% as the width, which sizes the image to fit the body content container. This is because simpler browsers do not support the method we are using to break the image out of the body container.

Simply declaring width twice will not work, because some simple browsers (ones we’re testing with) support vw units, they can read both declarations. We don’t want to artificially hide it behind some advanced feature like CSS variables as a hack, because that feature is not directly tied to the method we are using, which relies on calc(). A browser could support CSS variables and not calc(), or vice versa.

We want to test for the specific feature we want to use, and use either 100% or 100vw for the width depending on whether the browser supports calc(). @supports does the job.

Another example of a complex situation:

/* CSS Variables */
:root {
	--v-pad: clamp(.3em, 1vmin, .5em);
}

/* keep vertical padding */
p img {
	padding: 1vmin 0;
	padding: var(--v-pad) 0;
}

/* Pale Moon supports CSS vars but not clamp */
@supports (margin: var(--pad)) and (not (padding: clamp(.5em, 2vw, 1em))) {
	p img {
		padding: 1vmin 0;
	}
}

Here we ensure that images have a comfortable amount of space between them and any text. For simpler browsers, we add a variable amount of space between images and text with the vmin unit, which is 1% of the viewport’s smaller dimension. This is great in most cases; the only problem is there are no absolute limits on how small or large the padding space can get. In some weird edge cases with a very skinny and/or very large window, the padding could be unpleasantly small or large. We ask more complex browsers to use clamp() instead, which does more or less the same thing, but also sets lower and upper bounds for how much the padding size can change.

We already have a way to deal with browsers that don’t support CSS variables like --v-pad here: just use the previous, simpler duplicate declaration, no need for @supports. But what if a browser supports CSS variables, but not the clamp() we’re putting inside the CSS variables? Well, testing on Pale Moon shows it tries and fails to use the second, more complex declaration. So we need to check for browsers like that and tell them to do something else. So if a browser:

  1. supports CSS variables, and
  2. does not support clamp(),

then we insist that the browser use the simpler declaration, it’s not tall enough to ride our CSS variables ride.