Future proofing components within design systems

April 2021 | Permalink

Illustration by Barbara

When it comes to architecting web components within a design system, it is worth thinking about portability and future proofing. A design system should be able to evolve with the company’s offering and not be a product of technology hype or trend.

A design system should therefore aim at being technology agnostic.

With this in mind, some companies (ie: Microsoft Fluent UI) have implemented design systems as a foundation of html and css, along with their framework component implementation.

Build for one platform or for all. Everything you need is here. (Microsoft, Fluent UI)

As a framework fades in popularity and new technology comes along, companies can further develop their design system by adding a new framework implementation of the foundation layer. This architecture decision allows companies to invest safely in the foundation layer, which is based on native, dependable api.

Investing in the framework layer

Can companies invest safely in a framework layer? Given the history of frontend technology, I am tempted to answer no to this question. However, depending on resources and the product itself, it might be cheaper to invest in a framework ecosystem that is battle tested and well supported.

An alternative way to safely invest in framework layers is to assign more responsibility to the foundation layer, reducing this way the exposure to a certain framework however, it does come at a cost and may not suit all scenarios.

The exercise below explains how a foundation layer can be leveraged to reduce the exposure to frameworks.

The Core layer

A Core layer might be considered as a middle man between the framework and the foundation layer. The role of the Core layer is to encapsulate complex component logic and expose an api that can be imperatively consumed by the framework.

Let’s look at how this could look like in practice:

export default class Button<T extends HTMLButtonElement> {
    private element: T; 

    constructor(ref: T) {
        this.element = ref;
    }

    setLabel(label: string) {
        if (!this.element) return;
        this.element.textContent = label;
    }
}

This Button class provides a simple api to to set a label for the button element. Can this be implemented as a Web Component? It can. This pattern allows us to refactor the code as a web component however, there are some caviats that must be considered when doing so. For the scope of this exercise, a simple class will do the job.

The Framework layer

Now that we have a component class and an api, we want to think about how a framework such as React could implement it:

import React, { useEffect, useRef, useState } from 'react';
import { Button } from '@core/Button';

function UIButton({ label }) {
  const ref = useRef(null);
  const [instance, setInstance] = useState();

  useEffect(() => setInstance(new Button(ref.current)), []);
  useEffect(() => instance?.setLabel(label), [label, instance]);

  return <button ref={ref} />;
}

Other frameworks such as Vue, could easily implement the core layer using a similar technique. A native api effectively acts as a dependency-free facade.

Conclusion

Adding a Core layer to a design system foundation is optional but may be beneficial for those user interface components where there is a significant investment and its underlying complexity prevents a smooth migration to a different framework.

Such integration however may be expensive to maintain. Design effort is required to keep the framework layer thin compared to its core layer implementation and for some components the work may not be worth undertaking.

Useful Resources