Aligning Divs and Devs: getting a consistent front-end

by Natalie Dixon and Stefano Ceschi Berrini

Imagine discovering your house had subtly changed every time you came home. The cupboard doors open different ways. The light-switches control different lights, and the hot and cold taps in your kitchen swap places. The inconsistency would be maddening – and the same goes for apps and websites. Creating a consistent UI keeps your users sane and orientated. For any product where there are multiple developers, the ideal solution is a shared set of common elements and styles, but this is not simple to achieve. Aligning divs on a page can be tough; aligning developers on an approach to front-end development can be even tougher.

Sharing is not easy – it requires time and effort. It’s faster to build for immediate needs and defer investing in a shareable solution; without consolidation this soon becomes a maintenance nightmare.

At Tes, we have evolved an approach that helps us build a consistent front-end across a micro-services architecture. Every page you see at Tes.com is composed out of multiple fragments, each of which are created by (mostly) React applications that are worked on by many different teams. So how do we go about creating a clean, consistent UI?

Front-end United

At Tes, developers are empowered to push particular areas they feel strongly about by volunteering as Champions. Stefano Ceschi Berrini stepped into the Front-end Champion role partly to communicate about the different components he was building. “Front-end felt like it didn’t have enough focus,” he says. “I would share what I was building in a chatroom, but not everyone would see it.”

Stefano proposed having a cross-team monthly Front-end meeting, open to anyone interested. Many people were. This raised awareness of what work had been done, and began to reveal what more was needed. It was the first step towards building the front-end community necessary to create a consistent UI.

Sharing is caring

At Tes we share front-end code using several different micro-services, including:

  • Service-site-assets
  • Module-tsl-ui-components
  • Tes-sass

Our micro-services context led us to split them by responsibility, and by how much they were likely to change. They emerged organically when needed - for example, those responsible for providing the basic common template and assets for pages across tes.com came first.

Having separate projects meant that we could give different teams easy access to reusable styles and components by making them available as NPM modules.

Service-site-assets

Builds the basic skeleton of a page and provides shared elements, such as the masthead, footer and navigation. It also contains a common set of CSS rules that are applied across the whole website. 

Tes-sass

Contains variables, mixins, and other shared utilities, and gets imported into individual applications. Originally this contained shared components as well, before having its purpose defined more carefully as housing shared constants, rather than more changeable React components.

Module-tsl-ui-components

A repo for shared, flexible, reusable React components, helping teams avoid reinventing the wheel. Developers import both the component and the companion CSS into their projects. The components that live here need to be adaptable to multiple different contexts; they shouldn’t contain domain logic.

It’s also the source of our living style-guide, thanks to Storybook (more on that below).

Tes styleguide

Challenges

This approach has some technical downsides; for example:

  • Keeping CSS and JS separate means that we need to import both the React component and the CSS (more on why below).
  • Different services may use different versions of module-tsl-ui-components as dependencies. This introduces some inconsistency, but allows teams to make specific changes for different services.
  • Styles can be duplicated, as some styles for shared UI components are already in service-site-assets. We can also create duplication within a single service if we import the same Sass file into multiple Sass files belonging to different components.
  • There may be collisions between different application fragments when the same CSS is imported more than once on a page. A quick fix is to namespace any imports from module-tsl-ui-components, as below:
.namespace-class {
  @import '~module-tsl-ui-components/components/form/form';
  @import '~module-tsl-ui-components/components/buttons/tds-button';
}

This creates a lot of duplication, but we can live with it thanks to Gzip and its intelligent compression algorithm, via our CDN.

All of these solutions and challenges needed the awareness and input of multiple developers and teams. They did not happen at once, but step by step. Their life and improvement rest on our front-end community.

Tools for Understanding

At Tes, we’re generalists: we believe in full-stack development, rather than hiring developers who specialise in purely front-end or back-end. Any tools we use need to be accessible or forgiving to colleagues who are more back-end focussed, as well as helping us create a consistent, user-friendly UI.

Just CSS?

Although it bucks current trends, we find it helpful to maintain a more traditional separation of CSS from markup and code.

For now, we have rejected coupling CSS with React components, using Styled Components or CSS-in-JS. These approaches have much to offer. For example, CSS-in-JS can reduce duplication by analysing the rules used to create unique, Atomic CSS-style classes (one rule per class).

Unfortunately, this is difficult to apply in a micro-services context. Both Styled Components and CSS-in-JS generate unique class names by relying on having a single React component as a point of entry – which we don’t have. When different parts of a page are unaware of each other (as with Tes.com), there’s a limit to how much optimisation things like CSS-in-JS can do for us.

Introducing either approach could involve a substantial cross-team refactoring effort. It also adds another layer of abstraction to something that needs to be easily sharable and understood.

We therefore follow a more traditional separation of CSS from markup and code.

Not just CSS

Sass allows us to serve standard CSS to our users while making use of extra features, such as browser-agnostic variables, functions (mixins), the ability to nest selectors, and so on. These features allow developers working on individual services to easily make them consistent with the rest of Tes, and can also improve reusability and readability. For example, we can use nesting and the magic of Sass’s ampersand to easily group related styles together:

.tes-btn--primary {
  background-color: $color-ocean-blue-light;
  color: $color-white;
  &:hover, &:active, &:focus {
    background-color: $color-ocean-blue-dark;
    border-color: transparent;
    color: $color-white;
  }
}

// This outputs as

.tes-btn--primary {
  ...
}

.tes-btn--primary:hover, .tes-btn--primary:active, .tes-btn--primary:focus {
  ...
}

Nevertheless, Sass features have their downsides. We risk making the code difficult to search, and creating very brittle, long selectors, introducing specificity issues. It’s easy to forget the output CSS. We try to follow a rule of no deeper than two levels of nesting.

Additionally, we avoid the more obscure parts of Sass: our end goal is to enable every developer (including those who are not necessarily front-end-orientated) to easily change markup and styles. This concern also affects how we apply classes in individual projects.

Writing rules that target elements under a namespace is quick and easy:

// HTML

<div class="job-card">
    <h2>Title</h2>
    <span>Salary</span>
</div>

// Sass

.job-card {
  H2 {
    ...
  }
  span {
    ...
  }
}

Unfortunately, it also tightly couples our styles to our HTML. Someone improving the semantics of our markup could easily break the styling. It’s better to target descendant classes and also use the BEM (Block Element Modifier) convention:

// HTML

<div class="job-card job-card--gold">
    <div class="job-card__title">Title</div>
    <div class="job-card__description">Description</div>
    <div class="job-card__salary">Salary</div>
</div>

// Sass

.job-card {
  &--gold {
    ...
  }
  &__title {
    ...
  }
}

Telling the Same Story

To achieve site-wide consistency we needed a style-guide: a single source of truth that could be accessed by designers and developers, and which would be ever-green, reflecting the most up-to-date development work. We use Storybook – an interactive development playground and site generator – within module-tsl-ui-components to create the Tes style-guide.

Storybook allows us to provide interactive demonstrations of each UI component, giving clarity to developers and designers alike. It enables us to more easily work with designers to ensure components make sense to everyone, and follow design guidelines. We add new Stories when a component is adjusted or added.

Even the best style-guide is only useful with community involvement. After Stefano took it on as a hack-day project and used the monthly Front-end meetings to discuss it, others began to contribute. This in turn encouraged more cohesion generally across our front-end: JS teams that weren’t already using React moved to take advantage of the style-guide, and largely Drupal-based teams found ways of incorporating React components.

Making Time

“We’d get together and raise issues but not have time to work on them,” says Stefano. “I thought, what about having a Front-end day once a month? We get together and work together.”

Greater awareness of shared front-end resources made the business value clear. As a result, any interested Tes engineer can now spend one a day a month working on front-end projects, such as maintaining the style-guide or adding new components – adding to their front-end knowledge or sharing expertise along the way.

Pulling Together

Our approach has evolved as our teams ran into these issues and solved them one by one. We work to make re-use easy, and write code friendly to developers of all preferences. Regular meetings allow us to identify issues and then carve out the time to deal with them.

A cohesive user experience rests on good communication and cross-pollination: seeking out others who are interested in front-end issues, or offering to share enthusiasm and knowledge. If you work in a multi-team context, try finding the hidden front-enders in your company. Requesting a short, regular meeting can start raising awareness and preventing inconsistencies, without requiring much time away from developing features.

Start talking. Your users will thank you.