UI Design and Development for Software Engineers

How to Stop Worrying and Love the Grid

Cloudera engineering’s approach to user interface layout

Jason Golieb
Engineering@Cloudera

--

People standing on a grid.

How do you build an application that is functional, intuitive, and attractive? It’s a question of design. While “design” is something that we do in every phase of software development, this article will focus on the interaction between UI/UX designers and engineers.

First Impressions

When most users first encounter a visual design, the first thing they do is try to form a mental model of the overall layout that will help them understand how to begin using it. Designers use many tools, including size, color, weight, spacing, and alignment, to establish visual hierarchy. Visual hierarchy brings a sense of order to a design, and this helps users build their mental model quickly. When the visual hierarchy is designed poorly — or not at all — you end up with a type of design that I call “clown pants”. What do I mean by “clown pants”? Just look at the clowns’ pants below.

Two clowns with, shall we say, “loud” pants.
Breaking all the rules.

They are a visual cacophony, throwing colors, patterns, and imagery at you with no discernible organizational scheme — it’s just a bunch of colorful noise. Now, look at these examples:

Two design examples. The first is much more poorly designed than the second.
Clown pants versus tailored pants.

The one on the left uses two different background gradients, many different, hard to read fonts, oddly-shaped controls, and the spacing is totally inconsistent. It’s hard to know where to look first. In contrast, the example on the right drops the garish background and uses font variants from a single family. It uses spacing, font weight, font size, and alignment to establish a logical visual hierarchy.

Most people, even if they can’t explain why, will automatically recognize that the UI on the right feels easier to use and seems more professional. But achieving this level of polish can be a source of great frustration for engineers who don’t understand the “visual” in visual hierarchy. They may have a vision of how they think their UI should look, but they may lack the tools to achieve it, at least not without great struggle. Even if handed a high-quality design, they may feel overwhelmed at the thought of trying to build a pixel-perfect implementation. That’s where having a well-maintained design system and a corresponding component library that implements the design system comes into play.

At Cloudera, our design system is called Cloudera Design Language (CDL), and we have a React library, named CUIX, that provides components that follow the CDL specification. Of all the tools at the designer’s disposal, one of the most basic concepts is the visual grid, and CDL describes the grid that all Cloudera applications should use to lay out their UIs. CUIX implements the grid so that engineers do not have to repeatedly reinvent this functionality. This eases the design burden for engineers and makes developing new UIs relatively simple. In the rest of this article, I’ll show you how Cloudera approached the challenge of defining a grid system for our applications and how we implemented it in a way that helps developers “respect the grid” with minimal effort or design knowledge.

We’ve defined our grid system as part of Cloudera Design Language, the specification for all of Cloudera’s user experiences. It may sound obvious, but having a formal specification for design elements is important. You need to write this stuff down so that everyone has something tangible to look at and agree on. Then, you can get to implementation, secure in the knowledge that you’re doing the bidding of a higher authority — the almighty specification.

Defining the Grid

Simply put, our system is a twelve-column grid. Why twelve columns? Because the number twelve is magic. It’s not too big, not too small, and it’s divisible by 2, 3, 4, and 6, which provides many opportunities for breaking it down in different ways and still having things add up to twelve. There are 24 pixels of whitespace between each column (the gutters) and 24 pixels to the left and right of all the columns (the margins). The columns are allowed to grow to a maximum width and shrink to a minimum width, depending on the amount of available space in the browser window (the viewport).

What happens when the viewport gets smaller or larger than these limits? At the lower end, the grid stops contracting. As a result, part of the UI is forced outside the viewport (“overflow”) and we get horizontal scrolling. At the upper end, the grid stops expanding and remains centered in the viewport, with empty space extending to the left and right. Between these limits, the grid is “responsive”, that is, it grows and shrinks along with the viewport.

Sounds simple so far (maybe?), so let’s make it a bit more complicated. We need to leave space for anything that might take up space in the viewport outside the grid. That’s right: the grid is not necessarily the entire user interface. At Cloudera, we have a standard global navigation menu that takes up a variable amount of space and is fixed to the left side of the viewport. There is also the vertical scrollbar on the right-hand side to worry about.

Block diagram of the CDL grid and surrounding UI elements.
The CDL grid and surrounding UI elements.

This layout is made of components organized like this:

<ApplicationLayout>
<Banners />
<Navigation />
<PageLayout>
<TitleBar />
<GridLayout>
<Card />
<Card />
<Card />
...
</GridLayout>
</PageLayout>
</ApplicationLayout>

Let’s discuss each one.

ApplicationLayout

ApplicationLayout is the top-level container for the entire viewport. It defines space to contain any global banners that span the top of the viewport, space for the global navigation element on the left, and leaves the rest of the space for the page content. To create this component, we must define the relationships between its children. As mentioned, if there are banners to display, they must span the entire top of the viewport; everything else goes below the banner area. This is simple enough to do with a couple of divs.

Next, we need to lay out the navigation area and the content area, which sit side-by-side. Again, a couple of divs will work here.

You should also consider whether a more semantic HTML element, like nav, would be more appropriate and accessible. The concepts of semantic HTML and accessibility are outside the scope of this article, but you should definitely consider them as you build out your component library.

To get those block elements arranged horizontally, we contain them in a parent that uses flexbox.

Flexbox and Hierarchical Structure

Flexbox is a tool in the web development arsenal that has been around for a decade (it was supported by Chrome in 2013), and you’re probably already familiar with it, at least somewhat. Another relative newcomer among layout options is CSS Grid (supported by Chrome since 2017). You may be wondering what the main differences are and why we used flexbox here.

The main difference is that CSS Grid is a two-dimensional layout scheme, superficially similar to old-school tables. You define columns and rows and tell your elements which cells they should live in. In contrast, flexbox is a one-dimensional layout scheme. You tell flexbox whether you want it to arrange its children horizontally (flex-direction: row;, the default) or vertically (flex-direction: column;), and they are laid out in a line going in the corresponding direction. That line can be configured to wrap when it needs to. Or the flexbox’s children can be allowed to grow, shrink, or do neither, depending on how you want your content to flow and respond to changes in its surroundings. Although a web page, as a whole, is a two-dimensional design, in most cases you don’t want or need to lay it out in a two-dimensional structure. In reality, a web page is a hierarchy of one-dimensional structures.

If that’s not immediately clear to you, then remember the ApplicationLayout discussion above. The ApplicationLayout contains a vertical one-dimensional structure containing two children: the banner area and the remaining space. This remaining space contains a horizontal one-dimensional structure that has two children: the navigation area and the remaining space, which is designated for the page content. As you read through the rest of this article, you’ll see that the rest of the components are structured similarly.

It’s not just HTML that is structured this way. If you ever use the design tool Figma (and it’s a good bet that the UI/UX designers you know have!), you will see that the designs you build in Figma are also structured as a series of nested one-dimensional layouts.

To make the child of a flexbox take up all the remaining space, you can generally set flex-grow to 1. Other options are to set flex to auto or to set width or height to 100%, depending on the flex-direction. Note that in certain situations, each of these options will have the same effect, but in many situations they will not. Explaining the differences between them is outside the scope of this article.

PageLayout and GridLayout

Coming back to our component structure, next we have PageLayout, which is another vertically-oriented flexbox. It contains the TitleBar, which spans the full width of the PageLayout, and the remaining space is, finally, where the design grid lives, in the form of GridLayout. The GridLayout component’s job is to determine the width of each of the twelve grid columns. To do this, it has to account for the minimum- and maximum- width limits of the grid. The grid also has 24-pixel gutters between each column. The size of the grid columns, therefore, follows a simple formula:

column width = (grid width - gutters) / number of columns

The size of the gutters and the number of columns are values we define, but the grid width is a value we need to obtain in real-time from the browser. As previously mentioned, the grid must obey its minimum- and maximum-width constraints. Between these extremes, it is allowed to take up the entire width of the GridLayout. The GridLayout is bounded by the width of the viewport, less the 24 pixels of padding on either side and any external items that take up horizontal space; in this case, those are the navigation element and the vertical scrollbar, if present. So, to obtain the grid width, we need to look at the width of the GridLayout (the content box width, specifically) and we need to know whenever that changes.

ResizeObserver

The modern way to solve this problem is with ResizeObserver, an API provided by the browser that lets us watch an element and receive updates whenever its size changes. In React, the best way to use a ResizeObserver is via a hook, like useResizeObserver from beautiful-react-hooks (or any of several, similar libraries that offer such a hook).

To use useResizeObserver, you first create a React ref. You pass the ref to useResizeObserver when you call it, and it returns an object that contains several properties of the element that you bind the ref to, including the value we are interested in, its width. Finally, you pass the ref object to the ref prop of the HTML element you want to observe, binding it to that element.

You can also pass a ref to a React component, but that component must be created using React.forwardRef(). This gives the component the ability to accept a ref prop and internally bind it to an HTML element that it renders. Ultimately, the ref will end up bound to an HTML element. In general, refs can be used to store any mutable value, but using them to reference DOM elements is perhaps their most well-known use case.

With this value in hand, we can perform the column width calculation given above, but what do we do with the result?

Cards and GridContext

GridLayout uses a React context to provide the column width it calculates to any of its descendants that want to know it. This lets us avoid having to explicitly pass the value as a prop to each child. If we create some general-purpose container components that use the GridContext, as we’ve called it, to enable them to respond to grid size changes, we can put all kinds of actual content in those containers and have them live on the grid. And that’s exactly what we’ve done. We created a Card component that accepts a colspan prop, which, combined with the current column width it reads from GridContext, determines its own width using the formula:

card width = column width * colspan + gutter width * (colspan - 1)

Going back to GridLayout for a moment, that component is configured as a vertically-oriented flexbox. You might be surprised by that, since it is used to control the column width, and columns are by nature laid out horizontally. In this case, however, we are only using flexbox so that we can also use the gap style property to set a gap of 24 pixels between items as they stack up vertically. Vertical stacking is the way that block elements are laid out by default, and we want to maintain this natural flow, just enhanced with consistent vertical spacing.

How do we actually lay out more than one card on a row with the appropriate gutters? We created another type of container named CardListLayout that is simply a horizontally-oriented flexbox with the same gap of 24 pixels set. So, you can either stack up bare Cards vertically, stack up CardListLayouts vertically with Cards laid out horizontally inside them, or mix and match to create a layout that consistently “respects the grid.”

A diagram demonstrating cards laid out in the grid, some inside CardListLayout containers to achieve horizontal flow.
Cards flow vertically inside the grid. To achieve horizontal flow, we enclose them in a container with `flex-direction` set to `row`.

Conclusion

If you’re a software engineer who has ever felt lost or intimidated when confronted with the thought or the reality of building a UI, hopefully this article has given you a sense of direction. Although this article wasn’t particularly brief, it still only gave you a taste of how UI/UX designers approach this task. I enumerated some of the tools at their disposal and I went into some detail about how we implemented one of the most fundamental parts of the UI framework, the layout grid. By providing a solid implementation of your organization’s design specification, you can take much of the burden of design off of the engineers’ shoulders, letting them focus more on coding and reducing cycles of design review and rework.

Most importantly, by building a solid design language specification and an implementation of it that exposes a good developer experience, you can help ensure that your team or company’s products all look, behave, and feel consistent. In the end, it is this sharp focus on fit and finish that will build confidence in your products among their most important critics: your customers.

--

--

Jason Golieb
Engineering@Cloudera
0 Followers

Senior Staff Engineer at Cloudera who focuses on UI engineering and maintains the implementation of Cloudera's product design system.