Skip to main content

Building a UI component library: How we balance brand identity and speed

· 9 min read
Filip Tammergård
Software Engineer

Whether or not to roll your own UI component library is a common dilemma when developing products with a unique brand identity. In this blog post, we shed light on how we're solving this at Einride – and lessons learned from along the way.

At Einride, our goal is to eliminate the 7% of global carbon emissions that come from road freight. We have learned that making an impact is not just about developing cutting-edge technology - you also need a unique brand to make your products stand out in the market.

At Einride, we aim to convey our unique brand identity across software and hardware products.

It can be challenging to convey a unique brand identity in digital products, especially when going through a scale-up phase where experiences and experiments are perpetually developed and launched. Development speed is vital, which means that building brand-accurate UIs must be quick and easy.

In the early days of Einride, way too much time was spent on creating the most basic UI components – such as buttons and input fields – time and time again. This considerably impacted our development speed. Buttons can cost any company a lot of money.

We knew that having a component library of ready-made and brand-aligned components would significantly speed up development. That's why we went ahead and created Einride UI – our own component library. But this led us to another challenge: How do we strike a balance between investing time in developing a unique component library, versus investing time in developing the actual product?

How do you convey a unique brand without building components from scratch?

One of Einride's development principles is: "Build what we need, not what we might need". Considering the vast amount of excellent component libraries out there we could use, is building our own component library from scratch really the most effective approach? Or is there a better way?

Unique brand identity without building from scratch

Building elementary and reusable form and typography components was low-effort and high-impact – a certain win on the action priority matrix. However, there soon comes a point at which not-so-elementary components, such as sliders and tabs, are needed. The more complex the components that were built, the more numerous the bugs that were reported.

A very small portion of the time spent on these components was directed to Einride's unique brand identity; almost all of the time was spent on basic UI component logic. A high standard of accessibility was also required, so a fair amount of time was devoted to reading web content accessibility guidelines or related documents, such as the WAI-ARIA design patterns.

We started to question what we could do to minimize time spent on reinventing UI components while preserving Einride's strong brand identity. Enter Radix UI.

Radix UI is a brilliant library of headless components – meaning it provides no styles of its own, only the basic logic, combined with simple ways of applying styling according to your unique brand. Radix UI focuses on accessibility, and most primitives are implementations of specific WAI-ARIA design patterns.

Starting from headless components has made it possible for us to build components with a unique brand identity without losing time reinventing basic UI component logic.

Allow for changing requirements by enabling composition

The only constant at a fast-growing company like Einride is change. Requirements change. New projects replace old. Brand identity evolves. The nature of ever-changing requirements affects the development of a component library.

So how can the API of the component library be designed to avoid constant breaking changes and all the overhead that comes with it? Let's use a <Segments> component as an example.

A segmented control with two segments.

At first sight, the component contract could be designed as an array of strings, where each string is the text of a segment:

interface SegmentsProps {
segments: Array<string>
}

Which would mean it could be used by just passing two strings:

<Segments segments={["Kilogram", "Pound"]} />

Then requirements change, and an optional label after the text in each segment should be possible.

A segmented control with two segments, where the first segment features a label.

In this example, the component doesn't currently support this requirement. What's worse, it requires a breaking change to support. Let's change the interface to include an optional label:

interface SegmentsProps {
segments: Array<{
text: string
label?: string
}>
}

The above design can now be achieved by utilizing the label prop.

<Segments segments={[{ text: "Kilogram", label: "1" }, { text: "Pound" }]} />

Then requirements change, again, and the label needs to be – optionally – made accented.

A segmented control with two segments, where the first segment has an accented label.

The last breaking change was published recently and required a fair amount of effort to have teams upgrade to. We wouldn't want to redo the entire process with another breaking change just a few days later.

To support the new requirement without breaking component API, let's add a boolean accentLabel:

interface SegmentsProps {
segments: Array<{
text: string
label?: string
accentLabel?: boolean
}>
}

The new accentLabel can now be used:

<Segments segments={[{ text: "Kilogram", label: "1", accentLabel: true }, { text: "Pound" }]} />

You now enter a stage we might call boolean bolognese – you have designed an API that encourages the addition of a new boolean for every new requirement. It also lends itself to confusion, such as this prop combination:

<Segments segments={[{ text: "Kilogram", accentLabel: true }]} />

The label is set to be accented, but there is no label, right? Hrm. It's possible to define the interface to disallow such impossible states, but solving for allowed prop combinations with multiple booleans creates a typical boolean spaghetti bolognese mess – which, in turn, enables bugs to sneak in.

All this mess could have been avoided by relying on composition. Relying on composition is a way of recognizing that requirements will change by giving more power to the developer who is using the component library. Radix UI uses the same technique to provide headless components in the first place.

Here's what relying on composition could look like in the case of the <Segments> component.

interface SegmentsProps {
children: ReactNode
}

interface SegmentsItemProps {
children: ReactNode
}

interface SegmentsItemLabelProps {
children: ReactNode
}

And here's how to achieve each previous case:

// first case
<Segments>
<SegmentsItem>Kilogram</SegmentsItem>
<SegmentsItem>Pound</SegmentsItem>
</Segments>

// second case
<Segments>
<SegmentsItem>
Kilogram <SegmentsItemLabel>1</SegmentsItemLabel>
</SegmentsItem>
<SegmentsItem>Pound</SegmentsItem>
</Segments>

// third case
<Segments>
<SegmentsItem>
Kilogram <SegmentsItemLabel variant="secondary">1</SegmentsItemLabel>
</SegmentsItem>
<SegmentsItem>Pound</SegmentsItem>
</Segments>

No boolean bolognese as far as the eye can see! Allowing for composition is an effective way of avoiding component APIs that are bound to only increase in complexity and confusion as requirements change.

But what challenges does composition come with?

Trust developers to maintain brand identity

Relying on composition puts more power in the hands of the users of the component library – and with power comes responsibility. Accepting ReactNode means you can pass whatever you want into the components, and you can certainly use that power in ways that are not intended. For example, like this:

<Segments>
<SegmentsItem>
<div style={{ background: "red" }}>Kilogram</div>
</SegmentsItem>
<SegmentsItem>Pound</SegmentsItem>
</Segments>

But inversion of control is not necessarily a disadvantage.

When someone needs to adjust the styling of a component, they will do that. It doesn't matter that it isn't easy. There are many creative ways to override styling where it was not supported – ranging from targeting random HTML DOM nodes (that might change whenever) to copying the component code from the library to the app and making the tweaks there. Are these alternatives really better?

Most style adjustments originate from designers making decisions for specific use cases that might be perfectly valid for those cases, even though they aren't officially supported in the design system.

We believe it's better to trust developers to do the right thing. Trust them to go beyond the constraints of the design system only when they have good reasons for doing so.

Making the component library more flexible than the design system is also practical when it comes to collaboration between design system updates and component library development. The design system can be updated with just a few clicks in Figma. If those changes are breaking changes, however, they require a lot of work on the component library side related to helping teams upgrade to the new major version. As we mentioned, change is the only constant – and that includes breaking changes.

It's more practical for the design system to present the constraints while letting the component library allow for changing design system requirements, without needing breaking changes.

By trusting developers to take responsible decisions, the flexibility that comes with relying on composition can be seen as a strength rather than a problem.

Summary

Building our own component library has taught us many lessons on how to balance effort spent on developing uniquely branded components, versus effort spent developing a unique final product:

  • Start from a headless component library to avoid reinventing basic UI logic.
  • Allow for changing requirements by enabling composition.
  • Optimize for speed by trusting developers to do the right thing.

At a fast-growing company like Einride, new challenges arise continuously. We are currently figuring out how to optimize for speed in development even further. More on that another time!

Are you a developer or product designer who wants to create technologies for sustainable freight with world-class teammates in a high-trust environment? Check out our careers page.