DEV Community

Patryk Staniewski
Patryk Staniewski

Posted on

You might not need your own UI library.

Preface

Over the years, I came across a lot of different projects with diverse backgrounds and goals. From small, or even tiny in size and scope, to large monolithic applications with multiple frameworks and few layers of legacy code.
The vast majority of them had one key thing in common - they all had their own, custom library of UI components and various utils.

I will try to describe my experiences and propose alternatives using popular libraries and frameworks. I’ll try my best to describe the benefits and drawbacks of each scenario.

Startup - low cost, high ambitions.

When I joined this startup, let’s call it Guava to not use the real name, they were in the middle of launching their product to mobile (previously the application was only available for desktop users). It was supposed to replace their legacy app and was, for all the shapes and purposes, a real RWD. In the meantime, they had another website up and running - classical landing pages with homepage, about us, contact, and so on. They didn’t share a single line of code between them even tho they looked alike.

One of my responsibilities was creating a UI library. That way we could share and reuse the same components across both projects. We started with the design team. They prepared a style guide and described colors, buttons, inputs, etc. We discussed the details and created our own, beautiful and custom-made guava-core. It had a building blocks and some helpful utilities that could be used in all of our current and future projects.

Unfortunately, what I didn’t consider back then was the cost of developing this library. We spent few weeks discussing our APIs, another for initial implementation, another for polishing, and another for integration.

When new features came, they all were first added to our style guide. Some new variants for inputs here, a different hover state over there, a little icon in some of the buttons that weren’t used previously. We didn't want to simply add another set of properties to our components without a really good reason. We asked for a meeting to discuss these differences, sorted them out, but it took another few hours of our time that could be used elsewhere. And, we still needed to code new functionalities.

Our developer experience and user experience were good, great even. Our library had an elegant, extendable API that we based on Styled System. This doesn’t change the fact that we had to bid farewell to some of our team members, partially because of the rising costs of implementing new features, which increased a time-to-ship.

// Our custom fontScale prop
import { BoxProps } from '../Reflexbox';
import { system } from 'styled-system';

const transformFontScale = (props: BoxProps) => (value: any, scale: any) => {
  const { fontSize, lineHeight } = scale[value];
  const fonts = props.theme?.fonts ?? 'sans-serif';
  return `${fontSize} / ${lineHeight} ${fonts[0]}`;
};
export const fontScale = (props: BoxProps) =>
  system({
    fontScale: {
      scale: 'fontScales',
      property: 'font',
      transform: transformFontScale(props),
    },
  });

// <Box fontScale={[2, 4]} />
Enter fullscreen mode Exit fullscreen mode

Most of our components were kept small and by using atomic design we were able to extend them when needed.

Looking back however, I would definitely go for Material UI as the designs were loosely based on material design and with some compromises in both design world and from development standpoint, we could create more features faster and at a fraction of a cost, even with half the team we had.

Small company - design flexibility and development speed.

Development of this project started without a dedicated UI team. We had a rough idea of how it’s going to look like based on descriptions and small sketches of our creative director. We wanted to focus on developing core functionalities like sign-in & login, managing users, creating content, and so on. To speed things up, we decided to use Base Web (well, we started from material-ui, but we didn’t like its approach to styling back then).

Implementing our views was trivial, each component is heavily tested, both through e2e and unit tests. It has thorough documentation with lots of examples.

Sometimes later, the design came from a dedicated design studio. They were… let’s just say a lot, different than what we had in baseweb and they came a few weeks later than they supposed to. Because of that, we had a shorter time to adjust our frontend so we had to improvise.

As it turned out, extending baseweb was rather easy, because of its theming and overrides API. In our theme file, we defined the correct colors and customized some global overrides.

export const theme = createTheme(primitives, {
  colors: {
    inputBorder: primitives.primary100,
    inputPlaceholder: primitives.primary300,
    inputPlaceholderDisabled: primitives.primary100,
    tickFillSelected: primitives.primary500,
    tickFillSelectedHover: primitives.primary600,
    tickFillSelectedHoverActive: primitives.primary700,
    buttonPrimaryFill: accents.accent,
    buttonPrimaryHover: accents.accent300,
    buttonPrimaryActive: accents.accent200,
  },
  borders: {
    buttonBorderRadius: '4px',
    inputBorderRadius: '4px',
    surfaceBorderRadius: '4px',
    popoverBorderRadius: '4px',
  },
});
Enter fullscreen mode Exit fullscreen mode

We also created ui catalog in our project and made reexports from baseui/* to ui/*. That allowed us to then make overrides per component without changing its API or modifying its import path, for example, our extended tooltip looked like this:

import React, { FC } from 'react';
import { StatefulTooltip as BaseStatefulTooltip, StatefulTooltipProps } from 'baseui/tooltip';
import { mergeOverrides } from 'baseui/helpers/overrides';

export * from 'baseui/tooltip';

const statefulTooltipOverrides = {
  Body: {
    style: { maxWidth: '280px' },
  },
  Inner: {
    style: { fontWeight: 700 },
  },
};

export const StatefulTooltip: FC<StatefulTooltipProps> = ({ overrides, ...props }) => {
  return <BaseStatefulTooltip overrides={mergeOverrides(statefulTooltipOverrides, overrides)} {...props} />;
};
Enter fullscreen mode Exit fullscreen mode

We couldn’t however, override some styles globally, without extending each component separately, like border-width or font-weight for labels.
We decided that it would be more beneficial for us based on our team size (2 frontends and one full-stack) to create manual overwrites in one global CSS file.

/* --------- BASE WEB OVERRIDES -------------------- */
  [data-baseweb],
  [data-baseweb="checkbox"] > [role="checkbox"],
  [data-baseweb="select"] > div {
    border-top-width: 1px;
    border-bottom-width: 1px;
    border-left-width: 1px;
    border-right-width: 1px;
  }

  [data-baseweb="form-control-label"] {
    font-weight: 400 !important;
  }

  [data-baseweb^="typo-heading"],
  [data-baseweb^="typo-label"] {
    color: ${baseTheme.colors.primary700};
  }
Enter fullscreen mode Exit fullscreen mode

Yes, it’s a little bit nasty, but it’s really easy to investigate when something is being overwritten by this code in dev tools, is contained in one, tiny CSS file, and well... works like a charm.

We had a few situations, we couldn’t easily overwrite some of the design decisions. We reached out to our designers, and they were happy to help. We changed our UI just enough to not create custom components or large overrides to avoid maintenance costs and potential bugs.

The project was launched successfully and is being used by people worldwide, our codebase is tiny compared to what is happening under the hood in baseweb, is easy to test, and cheap to maintain.

Large corporation - everything custom-tailored to the product.

In larger companies, there is a strong tendency to do everything made in-house. The success of their UI libraries differs from company to company.

In some of my projects, we had a dedicated team responsible for creation and maintenance. In both of them, designs are created based on the core style guide, developers create new features using provided components. In case of missing an element or design not matching used components, a developer makes a request for changes in the library and waits. This workflow, even though it has some clear limitations and disadvantages, works well in really large projects, where the time to develop new features is a lot longer.

Other times, companies don’t see major value in these teams or are trying to reduce the costs of their IT departments. This is a long-term nightmare to maintain, as a large number of developers make changes and add components that will be used in their (sometimes very specific) use case without having a larger scope in mind. In this scenario, the codebase is getting ever larger without possible reductions insight without a huge amount of meetings and coordination between different teams. This is one of the major factors in having “legacy projects” and rewrites over time.

const ResetButton = styled(ButtonNoBackground)`
  display: flex;
  position: absolute;
  top: 0;
  right: 0;
  cursor: pointer;
  min-height: 48px;
  min-width: 48px;
`;
Enter fullscreen mode Exit fullscreen mode

In both cases, however, maintenance cost is really, really high. When a company chooses to pay for a dedicated team, they have to bear in mind a developer’s increasingly high salaries. Alternatively, when moving maintenance to individual developers ad-hoc, payment is made through a longer development time.

Closing thoughts

In my opinion, companies overvalue custom-tailored solutions and gloss over existing libraries based on hypothetical problems that might come in the future.
Oftentimes, increased pace to mark a product as “legacy” and rewrites every few years are not mentioned in initial meetings. Each of us wants to create the best possible product that will last forever, but that is not the case, especially in the javascript world. New features, frameworks, libraries, and patterns come almost weekly and we all want to work in the latest stacks.

Latest comments (0)