dio.la

Dani Guardiola’s blog

Jan 1, 2000January 1st, 2000 · 1 minute read · tweet

Why open/closed?

A deeper look into the advantages of open/closed components, and why you should Use The Platform™.

This article's main image

"The open/closed component" series

  1. The open/closed component

Use the platform

The web is a chaotic and beautiful amalgamation of technologies and APIs. Yes, it may be a mess, but it is our mess.

It is tempting to try and abstract away the platform, reinvent all of the wheels, and build neat little black boxes so you can forget about what's below. But that dream is not only impossible, it's also a nightmare. The web is too vast and too complex to be tamed.

Things used to be simpler a long time ago, but it's 2024 now. These days the web is, for the most part, "batteries included". Here are some of my favorite examples, including a lot of stuff we used to need custom abstractions or hacks for:

That is... a lot. Please don't ask how long it took me to compile this list.

This is not to say that you shouldn't build abstractions. You should, they are very useful!

The point is that life is easier when they are built on top of the platform, instead of as a replacement for it. At the UI level, this is precisely what open/closed components enable.

The three R's of UI

Most people are familiar with "the 3 R's": reduce, reuse, recycle. What not everyone knows is that these are in order of importance.

In terms of ecological impact, recycling something is great, but it's better to reuse it (e.g. by repairing it). And it's even better if you can find a way to not use it -or use less of it- in the first place (reduce).

With UI components, a similar logic can be applied:

Reduce

If there's an HTML element that does what you need, use it.

❌ DON'T

jsx
<div onClick={action}>
<div onClick={() => window.location = "example.com")}>

✅ DO

jsx
<button onClick={action}>
<a href="example.com">

Please? Thank you.

Reuse

If you truly need an abstraction, use a minimal version that can be easily reused and composed in many places.

❌ DON'T

Use moment.js for dates and times - it's big, monolithic, and has a lot of custom implementations of things that are already in the platform these days.

Read more.

✅ DO

Use native APIs like Intl.DateTimeFormat, Date, Date.toLocaleString... composed with minimal and modular libraries like date-fns.

Read more.

jsx
import { isFuture } from "date-fns";
function ConcertDateDisplay({ concertDate, artistName }) {
const formattedDate = new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "long",
day: "numeric",
}).format(concertDate);
return (
<div>
{artistName}'s concert
{isFuture(concertDate) ? " is scheduled for " : " took place on "}
{formattedDate}.
</div>
);
}

Recycle

When HTML elements and platform features fall short, or when you simply need styled versions of them, then it's time to build your own components. Choose a base HTML element or UI primitive, extend it with your own styles and behavior, and make sure it's open/closed.

❌ DON'T

jsx
<MyButton
action={{ on: "click-and-enter", callback: myCallback }}
tooltip="My tooltip"
tooltipPosition="top"
>
Click me!
</MyButton>
// renders a <div>

Since everything is custom, and we're using a <div>, the component needs to implement custom logic to make it focusable, accessible by keyboard, discoverable to screen readers, etc. We also need to implement a tooltip and pass all event listeners internally, as it's the only way.

✅ DO

jsx
<MyTooltip content="My tooltip" position="top">
<MyButton onClick={myCallback}>Click me!</MyButton>
</MyTooltip>
// renders a <button>

Since we're extending a <button>, we get all of the native behavior for free. We can also compose it with other components like MyTooltip, which uses the "render as" pattern to add tooltip trigger behavior to it.

This is only possible because <MyButton> is open/closed, and it can pass props like the required event listeners down to the underlying <button> element.


Beyond specific use cases, like the "render as" example from the previous section, open/closed components allow you to go about building your websites and web apps in a way that is more in tune with the platform:

  • First of all, there's the web. HTML, CSS, and JavaScript. The platform.
  • Then, add a little bit of UI framework. React, Solid.js, Svelte, Vue, Angular, or whatever you like. With a pinch of meta-framework (Next.js, Solid Start, Nuxt...).
  • Sprinkle some of your own styles on top. Maybe a couple of advanced features here and there. Open/closed components. Strong, accessible UI primitives from libraries like Ariakit or Radix. Nicer defaults. A few libraries to help too.

Compose it all together and you have a recipe for success:

  • Reasonable bundle sizes. The web does most of the heavy lifting.
  • No need to learn custom APIs for anything that HTML elements -or the underlying UI primitives- or web APIs already do.
  • It will work forever. The web doesn't break, by design. Your abstractions and libraries might.
  • It's fast and performant. Your app will feel snappy and responsive.

But what about the constraints?

This is the one and only concern everyone I've talked to about this brings up. I bet you're thinking about it too.

I get it. I've been there too.

When you're building primitives for other people to use, you want to retain control - if not, a lot of things can go wrong! On top of that, the web is messy, so abstracting away the chaos can be tempting.

A neat little family of black boxes, where absolutely everything is under control, and nothing can ever go wrong. That's the dream, right?

Since this is such a recurring reaction to the open/closed component pattern, I've dedicated a whole article to it in this series (coming soon, stay tuned). In a nutshell, in my humble (but informed by experience) opinion:

  • Thinking this is possible or even desirable is an instance of magical thinking
  • The (significantly larger) maintenance burden now falls on you.
  • Can't use new platform features.
  • It can backfire spectacularly and end up hurting the users, as I briefly discuss in the next section.

In essence: the web, but worse and with extra steps. Is it all worth it just so Dave from accounting can't set the wrong attribute on a button?

Let me tell you a secret: Dave will find a way. Just let it go.

TL;DR: Control is an illusion.

The case for accessibility

Probably one of the most important reasons to use open/closed components is accessibility.

The web is accessible by default. Native HTML elements, including form controls, buttons, links, labels, semantic elements, and more, are all accessible. They work with screen readers, keyboard navigation, and other assistive technologies. Out of the box!

This is not to say that you don't need to think about accessibility. Testing with screen readers, keyboard navigation, and other tools is still necessary. But you're starting from a good place.

When you extend these components, you inherit their accessibility features. The same thing applies to accessibility-focused UI primitives from libraries like Ariakit and Radix.

HOWEVER! Even if you do this, accessibility can still become challenging if components are black boxes. The web and UI primitives do a lot out of the box, but they aren't magic. A website or app is more than the sum of its components.

When designing and testing for accessibility, there's often a need to reach for specific platform features, like ARIA attributes, focus management, and other advanced techniques. If your components are open/closed, you can easily extend them to support these features, but when they're not...

Well, let me just say, good luck! You'll need to:

  • Involve the relevant teams to change all of the affected components.
  • Add the required features.
  • Think about how it fits into your custom API.
  • Test everything to ensure it doesn't break the rest of your neat little black boxes.

It becomes a chore,

"The open/closed component" series

  1. The open/closed component

Keep up with my stuff. Zero spam.

You're looking at my drafts!

Visit the main site: dio.la