Consistency and drudgery in UI design

Jasim A Basheer

The key to good UI design is visual consistency. It is also the key to good HTML & CSS.

But in practice, websites are inconsistent even in their brand colors. Their spacings will be all over the place; everyone rolls their own buttons; and text sizes will vary widely. Inspect a little, and most of these interfaces reveal no rhyme or reason. This absence of a coherent visual language is the foremost source of friction between designers and developers.

But it manifests in surprisingly subtle ways. Consider this design:

an inconsistent design

It looks pretty consistent, doesn't it? But see its actual measurements:

annotated inconsistent
design

The spacings between the boxes are off by a pixel, and the colors, while perceptibly the same, are also different from each other. If as a developer I have to construct a reusable component for the box, which values should I pick?

People are fine, tools are lazy

This kind of inconsistency is very common in real-world design files. But that is not because designers are lazy. The vector design tools they use are free-form and offer little affordance in keeping designs normalized. So every change, however minor, screws up things, and the designer has to manually fix them to maintain consistency. Here's an example:

On the browser, as an element grows in height, everything below it will automatically move down to make space. But this doesn't happen in free-form UI design tools like Sketch, Figma, and Adobe XD. Instead, the designer has to enlarge the box, move the text, and ensure that the original spacings are maintained. That is very tedious and lapses are bound to happen when you do this hundreds of times a day.

So it is no wonder that design files are imperfect, and it is going to remain the case until vector design tools start shipping with a layout engine[1]. But these inconsistencies puts the developer in a bad place when they try to code up the design. For every inconsistency, they have to either:

  • Pick the values exactly as they are in the design, consistency be damned. Or --

  • Arbitrarily decide canonical values so the front-end code remains clean

Neither one is satisfactory. In the first case I'm degrading my codebase with magic values[2], and that is a trap:

Front-End Developers tend to assume that there is some very important reason each design is exactly as it is. They assume that the difficult-to-implement clock dial has to look exactly like that initial design because they view all the UI Designer's work as that of an expert. They therefore don't try to simplify or discuss. They just assume there is some clever reasoning behind everything & that the UI Designer will create the perfect solution on the first attempt

-- Henry Latham, How to Save UI Designers & Front-End Developers up to 50% of Their Time.

But despite that if I still want to faithfully reproduce the design, then my CSS will look like this:

.box1 {
margin-bottom: 26px;

.author {
color: #3c0a0a;
}
}

.box2 {
margin-bottom: 27px;

.author {
color: #3e0c0c;
}
}

This is terrible code. Next time someone comes along and has to add another box, which one of the two will they duplicate? They'd be wondering what divine purpose underlie the 1px difference in margins, and cursing whoever wrote it in the first place.

This sort of blind, pixel-perfect translation of designs into code is how most CSS codebases start to become incomprehensible balls of mud that programmers fear to touch.

But if the developer normalized the values themselves, the code will become cleaner:

.box {
margin-bottom: 26px;
color: $title-red;
}

However, this isn't as easy as it looks -- it is often ambiguous as to which value should become canonical. Picking the wrong thing can mess up important design details and lead to painful back-and-forths with the designer.

Large teams manage but small teams struggle

This problem of inconsistency in designs and the difficulty faced by developers in disambiguating them, remains the biggest source of friction in front-end teams. If we can get rid of it, we'll eliminate the need for constant redlining and rework, improving morale, and increasing productivity.

Large organizations solve it with design systems. They allow designers to assemble new pages from a library of components rather than creating everything from scratch every time. Developers can build those designs equally fast since the components are already built and can be put in production immediately. Since these components are reused across the organization by dozens of people, each component justifiably receives large ongoing investment. This allows for great care and precision in their UI design and they are thus protected from mistakes and inconsistencies.

But small teams cannot afford full-fledged design systems. They often start building component libraries and invest months of effort, only to realize much later the true ongoing cost of maintaining it. [3]

There is however a solution -- a light-weight approach to both design and designer-developer collaboration that can eliminate these problems completely and is a breeze to follow for even solo practitioners. I think of it as the explicit design scale approach to building user interfaces.

The explicit design scale approach to building user interfaces

A design scale enumerates every possible visual style that can be allowed in a project. It includes visual elements like typography, colors, spacings, and borders. It must be both human-readable and machine-readable. Here's an example:

{
textSizes: {
"12": "12px",
"16": "16px"
},

colors: {
"black": "#000000",
"blue0": "#094771",
"blue1": "#0e639c"
},

fonts: {
"roboto": "Roboto, sans-serif"
},

margin: {
"auto": "auto",
"2": "2px",
"16": "16px",
"32": "32px",
"48": "48px"
}
}

When following the explicit design scale approach, the designer must define a scale upfront and adhere to it in the design. Similarly, the developer must explicitly encode it in their codebase and ensure that any CSS style in the code can only be defined based on the values in the scale [4].

But wouldn't this constrain the design unnecessarily? Adam Wathan and Steve Schoger addresses it in Refactoring UI:

You shouldn't be nitpicking between 120px and 125px when trying to decide on the perfect size for an element in your UI.

Painfully trialing arbitrary values one pixel at a time will drastically slow you down at best, and create ugly, inconsistent designs at worst.

Instead, limit yourself to a constrained set of values, defined in advance.

Or as they say in Zen:

In the beginner's mind there are many possibilities, but in the expert's there are few.

-- Shunryu Suzuki

The Zen master wants us to be open to many possibilities by always keeping a beginner's mind. But in design, great interfaces are created only by actively limiting possibilities.

Explicit scales saves work for both the designer and the developer

When a developer encounters an inconsistent value in a design, they can easily look up the scale to disambiguate it. Imagine that you're trying to write HTML & CSS for the following design and you encounter the 39px margin between the two elements:

annotated gap between two text layers

In the absence of a scale, you'll have to either pollute your code with the magic value of 39px, or you will have to talk to your designers and clarify whether they need that exact same value there.

But if there is a scale that the designer and developer has already agreed to, then we can just adopt one of those values. Given the below scale, we'll snap the 39px margin to the value closest to it, which is 32px.

margin: {
"auto": "auto",
"2": "2px",
"16": "16px",
"32": "32px",
"48": "48px"
}

This frees up the designer as well -- since they know that the developer can tolerate ambiguities easily and correctly, they can focus on the actual design work rather than having to worry about minor issues that need constant manual cleanup.

Adopting explicit design scales in your next project with Tailwind CSS

Design scales don't need to be sold to designers; it is already a part of their mental model. If not made into an explicit styleguide, it exists at least implicitly -- ask them "what is our usual padding for buttons?", and they'll have a pat answer. "what about larger buttons?" and they'll tell you the next value in their spacing scale.

Front-end programmers however are sorely lacking in this understanding; it reflects in the guaranteed devolution of most CSS codebases and the fraught relationship with designers. The fastest way for programmers to bridge this gap is to use a front-end programming environment that makes design scales explicit and prevents them from using magic values that are not part of the scale. This is best done by Tailwind CSS, the most powerful functional CSS library available today.

In Tailwind, the source of truth is tailwind.config.js, which looks like this:

{
textSizes: {
"12": "12px",
"16": "16px"
},

colors: {
"black": "#000000",
"blue0": "#094771",
"blue1": "#0e639c"
},

fonts: {
"roboto": "Roboto, sans-serif"
},

margin: {
"auto": "auto",
"2": "2px",
"16": "16px",
"32": "32px",
"48": "48px"
}
}

Tailwind generates all the CSS classes we need from this scale. Here's an example markup that uses these generated CSS classes, based on the above scale:

<div class='text-16 text-blue0 roboto m-16'>Hello world</div>

It means this:

{
/* text-16 */
font-size: 16px;

/* text-blue0 */
color: #094771

/* roboto */
font-family: Roboto, sans-serif

/* m-16 */
margin: 16px;
}

When using Tailwind, the programmer rarely has to write their own CSS classes or ad-hoc styling. This means it is almost impossible to write markup that doesn't fit into the scale [5].

This approach of using only single-purpose CSS classes has many names - Functional CSS, Utility CSS, Immutable CSS, and Atomic CSS. It is an emerging pattern which runs contrary to received wisdom in front-end programming. But it is a sound abstraction that grows on you and is fast to build, understand, and maintain.

Tailwind CSS in particular, with its ability to generate CSS classes based on custom design scales, lends itself extremely well to projects that have custom-built UIs made in vector drawing tools like Sketch or Figma.

Tailwind has extensive documentation and an active community and you can learn more in its home page.

Snap to Scale

This post came from our experience building Protoship Codegen, a tool that converts your Sketch designs into the perfect HTML & CSS. We saw hundreds of real-world designs during this time and saw that they were replete with inconsistencies. Translating them faithfully would've lead Codegen to generate clumsy code. So we adopted design scales as a core abstraction in the software, and included a feature we call Snap to Scale which snaps all values in the designs into a user-defined scale. We couldn't have been more happy with the result -- check it out in the post Snap to Scale.

In the meantime, if you have thoughts or stories to share, do let me know in the comments below.


  1. Vector design tools already ship with the beginnings of a layout engine -- it can be found in resizing rules and pinning constraints, which allows symbol instances to take different sizes than its master. But to fully eliminate drudgery from UI designers' lives, they also need to incorporate DOM's box model and flexbox. But it won't be straightforward to build -- keeping the design experience seamless across free-form and layoutted elements is a UX design challenge. It is however, in my opinion, one of the most important features that should be on every design tool's roadmap, and thankfully a new generation of design tools are already experimenting on it. ↩︎

  2. Magic numbers are just plain numbers that don't tell us anything about its context. Here's a CSS expression with a magic number: calc(100vh - 259px). What does that mean? Instead, how about calc(100vh - var(--footerHeight)) ? ↩︎

  3. A Design System isn't a Project. It's a Product, Serving Products by Nathan Curtis. ↩︎

  4. If we use a development methodology that embraces design scales, like Tailwind, a functional CSS library, then it will also eliminate an entire class of visual bugs from our software -- there will be no way for an errant color or spacing to show up in the UI because explicit design scales make those kind of bugs impossible to occur. ↩︎

  5. "Make invalid states impossible" is the best advice I've ever got in a decade or so of being a professional programmer. It means we should structure our data in a way that it can represent only valid values. Don't be fooled by the simplicity of the technique -- it is a force multipler for software quality and programmer happiness. For a lively demonstration, watch Richard Feldman's talk "Making impossible states impossible". ↩︎