Creating API for components: flexbox layout

alvechy

Alexander Vechy

Posted on May 7, 2021

Creating API for components: flexbox layout

How many times did you write display: flex? This goes so common that some people tried applying display: flex to almost all elements on the page.

In this post we will go through the thoughts process behind the API decisions for the most used component.

I've been thinking to write this for a while as I continue to see different implementations of a flexbox component, each with own API. I think we should stop inventing and standardize this.

New standards is born in an attempt to unify the standards

xkcd: Standards (https://xkcd.com/927/)

Start

In this article I'm going to use React and stitches (I am still in love with it). But the main idea of this article is to justify the API decisions that can be then applied in Vue, Svelte, Lit, or any other front-end tool.

Let's start simple:

import { styled } from '@stitches/react'

export const Flex = styled('div', {
  display: 'flex',
})
Enter fullscreen mode Exit fullscreen mode

For the sake of simplicity, I'm using pre-configured styled directly from stitches, but I in your libraries I encourage to use theme tokens for consistent layout properties, colors, font sizes, etc.

Wrapping

Let's start simple and add flex-wrap control:

import { styled } from '@stitches/react'

export const Flex = styled('div', {
  display: 'flex',
  variants: {
    wrap: {
      'wrap': {
        flexWrap: 'wrap',
      },
      'no-wrap': {
        flexWrap: 'nowrap',
      },
      'wrap-reverse': {
        flexWrap: 'wrap-reverse',
      },
    },
  },
})
Enter fullscreen mode Exit fullscreen mode

I'm using stitches variants that produce nice TypeScript props for Flex component

That was the simplest API decision to make, we only removed flex word to avoid repetitiveness, because all props exist in the context of Flex element already. Bear in mind, that the default browser value is nowrap, so using <Flex wrap="wrap"> can be a common thing. Although it might feel weird, it's still easier to learn and use (like flex-wrap: wrap), comparing to a made-up API.

Flow direction

Let's move on to the second prop: flex-direction.
I've seen direction used in some Design Systems, but for some people (me) it can be even worse than writing cosnt, especially because it's a commonly used prop.
Other Design Systems incorporate Row and Column components – they provide great context for the consumer:

// Flex defined as before

export const Row = styled(Flex, {
  flexDirection: 'row',
})

export const Column = styled(Flex, {
  flexDirection: 'column'
})
Enter fullscreen mode Exit fullscreen mode

Although now we also need to handle the cases when we want to use flex-direction: row-reverse; // or column-reverse. So, we either add reverse boolean prop (since it's not a common prop to be used):

// Flex defined as before

export const Row = styled(Flex, {
  flexDirection: 'row',
  variants: {
    reverse: {
      true: {
        flexDirection: 'row-reverse'
      }
    }
  }
})

export const Column = styled(Flex, {
  flexDirection: 'column',
  variants: {
    reverse: {
      true: { // neat way to create boolean variants in stitches
        flexDirection: 'column-reverse'
      }
    }
  }
})
Enter fullscreen mode Exit fullscreen mode

... or we define flow direction directly in the Flex component:

export const Flex = styled('div', {
  display: 'flex',
  variants: {
    wrap: {}, // defined as before
    flow: {
      'row': {
        flexDirection: 'row',
      },
      'column': {
        flexDirection: 'column',
      },
      'row-reverse': {
        flexDirection: 'row-reverse',
      },
      'column-reverse': {
        flexDirection: 'column-reverse',
      },
    },
  },
})
Enter fullscreen mode Exit fullscreen mode

As you might know, flex-flow is a shorthand for flex-direction and flex-wrap, so we're not making up the API again, but adopting it.

The usage so far would be (overriding browser defaults):

<Flex flow="row-reverse" wrap="wrap" />
<Flex flow="column" />
// or with dedicated components
<Row reverse wrap="wrap" />
<Column />
Enter fullscreen mode Exit fullscreen mode

Which API you like the most is up to you, both of them work great. I would prefer having just Flex or having all 3 of them. Flex itself is easy to maintain and it provides enough context straight away looking at flow prop, especially when it needs to change based on screen size, using response styles:

<Flex flow={{ '@tablet': 'row', '@mobile': 'column' }} />
Enter fullscreen mode Exit fullscreen mode

Imagine doing this with dedicated Row and Column components.

Alignment

So, making quite a good progress here, let's move on to the most interesting part: alignments.
While the official API for this would be to use justify-content and align-items, I always thought that both of these words make little sense to me when writing CSS. Maybe it's because I'm not a native English speaker, or maybe they don't make much sense when thinking about flex boxes.

One of the greatest articles that helped me to understand these properties was A Complete Guide to Flexbox (most of us still referring to). It has awesome visualizations that show how these properties affect items positions by the change of what is called main axis and cross axis. What really helped me though, was flutter's Flex widget. It has these two awesome attributes: mainAxisAlignment and crossAxisAlignment and the usage is:

Flex(
  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  crossAxisAlignment: CrossAxisAlignment.end,
)
Enter fullscreen mode Exit fullscreen mode

What's really great about this API, is that it's really easy to visualize in your head. If you have a row, your main axis is horizontal, if you have a column, it is vertical. So, no matter the direction, you can imagine your items evenly spaced on the main axis and aligned to the end of the container on the cross axis.

flex main axises for row and column

CSS-tricks: Directions (https://css-tricks.com/snippets/css/a-guide-to-flexbox/#flex-direction)

Knowing this, we can incorporate new API into our own component:

export const Flex = styled('div', {
  display: 'flex',
  variants: {
    wrap: {},
    flow: {},
    main: {
      'start': {
        justifyContent: 'flex-start',
      },
      'center': {
        justifyContent: 'center',
      },
      'end': {
        justifyContent: 'flex-end',
      },
      'stretch': {
        justifyContent: 'stretch',
      },
      'space-between': {
        justifyContent: 'space-between',
      },
    },
    cross: {
      start: {
        alignItems: 'flex-start',
      },
      center: {
        alignItems: 'center',
      },
      end: {
        alignItems: 'flex-end',
      },
      stretch: {
        alignItems: 'stretch',
      },
    },
  },
})
Enter fullscreen mode Exit fullscreen mode

Comparing to flutter's Flex API, I shortened mainAxisAlignment to main and crossAxisAlignment to cross. While TypeScript provides great autocomplete experience, seeing these long property names when composing multiple Flex components was hurting my eyes. Since both of the properties exist in the context of Flex component, I believe it's enough to understand them.

Now, the usage would be:

<Flex flow="column" main="space-between" cross="center" />
Enter fullscreen mode Exit fullscreen mode

flex-column example

The thought process for this component is fairly easy (or the one you can get used to): it's a column, so items will be evenly distributed across the main axis (y), and across axis x they will be centred.

By the way, new Chrome Dev Tools flexbox visual debugging is awesome.

Spacing

Now, the last prop we need to add is the one that controls spacing between the child elements. There were two approaches, generally: no-side-effects-but-nested-divs-one which wraps every children into box with negative margins to allow proper wrapping behaviour without changing the child nodes styles, and flex-gap-polyfill one, that changes the styles of its children through > * selector. Gladly, we don't need to talk about them today, since Safary 14.1 was the last one among the big folks to be released with the support of flexbox gap property. Thankfully, Apple is quite pushing in regards to updates, so we can see global browser support growing pretty fast.

export const Flex = styled('div', {
  display: 'flex',
  variants: {
    // the rest of the variants
    gap: {
      none: {
        gap: 0,
      },
      sm: {
        gap: '4px',
      },
      md: {
        gap: '8px',
      },
      lg: {
        gap: '16px',
      },
    },
  },
})
Enter fullscreen mode Exit fullscreen mode

Few things to comment here. First, you can still use pollyfilled option here, see an example from an awesome Joe Bell. Second, use xs, sm, etc tokens only if they are already incorporated in your Design System, otherwise, you may consider TailwindCSS number-tokens instead. Third, we don't implement powerful row-gap and column-gap CSS properties, but you can do them same way as for the gap. Third, we keep 'none' option to be able to set gap conditionally in a clear way, for example through @media breakpoints: gap={{ '@desktop': 'none', '@tablet': 'lg' }}.

End

And that's it! I really hope that more and more people would start seeing their UIs as a composition of layout and interactive elements, writing very little of CSS.

You can see some usage examples here. As with many things, you get the taste in the process, so feel free to play around with the playgrounds, see how such props help your intuition with visualizing the items.

Full example
import { stlyed } from '@stitches/react'

export const Flex = styled('div', {
  display: 'flex',
  variants: {
    wrap: {
      'wrap': {
        flexWrap: 'wrap',
      },
      'no-wrap': {
        flexWrap: 'nowrap',
      },
      'wrap-reverse': {
        flexWrap: 'wrap-reverse',
      },
    },
    flow: {
      'row': {
        flexDirection: 'row',
      },
      'column': {
        flexDirection: 'column',
      },
      'row-reverse': {
        flexDirection: 'row-reverse',
      },
      'column-reverse': {
        flexDirection: 'column-reverse',
      },
    },
    main: {
      'start': {
        justifyContent: 'flex-start',
      },
      'center': {
        justifyContent: 'center',
      },
      'end': {
        justifyContent: 'flex-end',
      },
      'stretch': {
        justifyContent: 'stretch',
      },
      'space-between': {
        justifyContent: 'space-between',
      },
    },
    cross: {
      start: {
        alignItems: 'flex-start',
      },
      center: {
        alignItems: 'center',
      },
      end: {
        alignItems: 'flex-end',
      },
      stretch: {
        alignItems: 'stretch',
      },
    },
    gap: {
      none: {
        gap: 0,
      },
      sm: {
        gap: '4px',
      },
      md: {
        gap: '8px',
      },
      lg: {
        gap: '16px',
      },
    },
    display: {
      flex: {
        display: 'flex',
      },
      inline: {
        display: 'inline-flex',
      },
    },
  },
})
Enter fullscreen mode Exit fullscreen mode

Key takeaways:

  • keep the API as close to the official specs as possible, making it easy to learn
  • make up own API is possible, but maybe there's some solution that is fairly common and people are used to it
  • learning other tools, like Flutter can open new perspectives
💖 💪 🙅 🚩
alvechy
Alexander Vechy

Posted on May 7, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related