Here’s how we build websites and web applications in 2021.
HyperText Markup Language
It’s a real shame how abused and neglected HTML has gotten in some corners of the industry over the years. Our skin crawls at the mere hint of HTML as a low-level “machine format” never intended for reading and writing by mere mortals. So we’re going to go out on a limb and declare that HTML—in all its piecemeal and compromised glory—is the greatest “semantic communication syntax” ever invented by humanity.
Despite its many critics, HTML has not only survived against the onslaught of numerous competing systems over its 30+ year history, it has so solidly established itself as a universal and boundary-defying cultural treasure that we have no doubt HTML will still be with us 100 years from now.
We prize and value HTML. Everything starts with HTML. Before we consider what CSS or JS frameworks to use, what build tooling to introduce, where to store and retrieve data, how to deploy the final product, and so forth, we start with the breadth of meaning and functionality we wish to express through HTML. It’s the baseplate of all web development.
As we begin to build out the structure of the site in generalized templates, we consider which built-in and custom elements we’ll need for expressing the intent of each section of each page. There are many built-in elements in HTML and they should always be prioritized wherever suitable.
<nav> for a navbar.
<h1> for a primary heading.
<article> to represent a unit of content. But for situations where a custom element is required, we will liberally define and use those throughout our projects—tag names such as
<main-content>. (Here’s a definitive article on the topic by Whitefusion founder Jared White.)
We’ve essentially stopped using
The emergence of the web components standard is perhaps the greatest leap forward for HTML since the arrival of the
<img> tag. With web components, you can program bits of encapsulated functionality which are able to be embedded in any web page “natively”.
For example, HTML provides a
<textarea> tag. But anyone could write their own
<fancy-textarea> tag which uses either
<super-dee-dooper-textarea> because of the capability each component affords, not because of its implementation details.
Token-based Semantic CSS Design Systems
Building upon the semantic elements and web components methodology above, we have recently made a fundamental upgrade to our approach to CSS (Cascading Style Sheets). It’s a methodology so new we’re not entirely sure there’s a name for it yet, but it all starts with CSS Custom Properties/Variables and some forward-looking syntactic sugar provided by PostCSS.
We start by defining a series of “tokens” as custom properties defined on
:root in a global stylesheet. We often co-mingle them with tokens imported from a library such as Open Props or Shoelace (more on that below). Example tokens might be
--primary-color: #ff6f59, or
--max-content-width: 50rem. We even create tokens for responsive breakpoints (not yet browser-native, but enabled by PostCSS). You can see these sorts of
:root-based design tokens on this very website by opening your developer inspector.
After a basic design system is in place, we begin creating styles using only element names as selectors.
main, etc.—as well as custom elements such as
navbar-inner. We use classes sparingly (no
.foo.bar .baz here!) while readily reaching for attribute selectors, especially for custom elements, e.g.
sl-input[size="medium"]. Occasionally we might override design tokens for particular element scopes, or within responsive media queries. In addition, when using web components which offer CSS Shadow Parts for advanced styling, we’ll use those as well when strictly necessary (
sl-dialog::part(title) for example).
This combination of CSS Variables, element/attribute selectors, and the mechanisms provided by Shadow DOM + Parts has resulted in a shocking reduction in the amount of CSS we write as well as import. In the past you couldn’t do much quickly without reaching for something like Bootstrap. Lately that would be the main selling point of Tailwind. (Er, use with extreme caution!). However, we increasingly find ourselves not needing any “CSS framework” at all…only some simple boilerplate and typically a web component-based UI library such as Shoelace.
Created by Cory LaViska, Shoelace is the most impressive unified collection of web components and design tokens we’ve seen to date. That’s saying a lot considering there are component libraries out now from Salesforce, Microsoft, Google, Adobe, GitHub, and many other large companies.
Shoelace at first glance might seem like Yet-Another-Bunch-o’-Components with the usual suspects of buttons, icons, menus, and dropdown—however, such simple appearances can be deceiving. What makes Shoelace so impressive are five things:
- It looks great right out of the box.
- It takes full advantage of modern web component standards.
- It’s extremely customizable, but only if you really need to.
- The HTML you write using Shoelace is fantastically elegant.
- Shoelace ships with a comprehensive set of design tokens you can use directly.
A button in Shoelace is
<sl-button>Hi!</sl-button>. An icon is
<sl-icon name="person-circle"></sl-icon>. A star rating is
You can customize how Shoelace looks simply by overriding various design tokens via CSS variables, and you can also use Shoelace tokens directly in your own styles and markup—even inside of inline styles! For example:
<h1 style="margin-bottom:var(--sl-spacing-2x-large)">Lots of Space</h1>
Shoelace v2 is a total rewrite and currently in beta, so as usable and impressive as it is now, this is only the beginning. We’re excited about choosing Shoelace as the default UI library for all our latest projects.
Bridgetown, Roda, or Rails:
Pick Your Flavor
Now that our basic frontend stack is set, let’s turn our attention to the backend. We unabashedly employ Ruby for its natural language-like expressiveness and object-oriented intellectual rigor. Ruby was designed from the ground-up to maximize programmer happiness and endow tiny teams with fantastic productivity. It’s our “force multiplier” secret sauce to get more done with less.
When evaluating the backend technology for a project, we start by examining the type of project, how it will be used, and what is expected by its audience. Working backward from user experience, we choose the appropriate developer experience we believe makes the most sense.
Typically, if the site is a “publication”…meaning it’s essentially content-driven (e.g., marketing sites/brochureware, blogs, educational destinations, etc.), then we’ll go the static site route and use Bridgetown.
If the site is primarily a dynamic application orbiting a database—requiring user authentication and up-to-the-second live data—we’ll build out a fullstack Ruby on Rails app using Hotwire-style techniques and components to provide a sophisticated interactive interface.
Increasingly though, we’re finding that Bridgetown’s static-first approach coupled with dynamic routes powered by its integration of the Roda web toolkit is a potent combination for a variety of applications. Never fear, we’re not leaving Rails behind. We’re just taking the bits we like and paring them with a dramatically different take on Ruby web development. Best of both worlds? We sure hope so.
In a break from conventional wisdom, we generally shy away from relying on third-party APIs for functionality. Sure, using APIs for things like email delivery or error monitoring is pretty much a given. Otherwise we believe in the value of building and hosting features in-house. That being said, there is an external API we add to virtually every project we work on—no matter the stack—and that is Cloudinary.
Cloudinary, simply put, is a CDN and live transformation service for images and videos. You upload high-resolution, large-size images to Cloudinary, and then you ask Cloudinary back for exactly the size, quality, and format you need. They all get transformed on the fly and subsequently cached for maximum performance. Files can be stored and retrieved solely through Cloudinary’s API, or they additionally provide a full web-based UI where you can view, edit, upload, and delete images. It’s about as awesome as image management can get.
As a shorthand method of authoring textual content intended for HTML generation, Markdown has become a defacto standard. We love Markdown, and we constantly reach for tools (both native apps as well as web services or libraries) that make working with Markdown a delight. We’ve helped train non-technical content authors how to utilize Markdown effectively, and we’re committed to building better editing experiences that take Markdown to greater heights without sacrificing the developer story.
The story of web hosting has changed dramatically over the past few years. Between hosting companies like Render or Vercel offering essentially free hosting for static sites, you can get incredible performance and full scalability and security at minimal cost. And for more powerful dynamic Ruby applications, convention-over-configuration hosts such as Render, Fly.io, and Railway are changing the way we deploy applications by focusing out-of-the-gate on simple framework detection, containerization, and multi-region availability.
This is a rapidly-evolving space and what makes sense today might not make sense tomorrow. Generally speaking, we’re not super-thrilled with that sort of churn. But we’re bullish on next-gen hosting solutions which place zero-config or easily-configurable build artifacts at their core. We’re trying our best to work out the kinks, document our findings, and make this easier for everybody. May the best cloud win.
For more information on some of the tools we use and build on projects like these, visit our Tech Specs page. Or visit Resources to learn more about applying techniques such as these to your own projects.