Creating API for components: flexbox layout
Tags: react, javascript, css, web
Published: 2021-05-07 22:26:59

How many times did you write `display: flex`? This goes so common that [some people](https://www.youtube.com/watch?v=Dk1gxZ_K3E0) 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.

<figure>
<img src="https://imgs.xkcd.com/comics/standards.png" alt="New standards is born in an attempt to unify the standards" />
<figcaption>xkcd: Standards (https://xkcd.com/927/)</figcaption>
</figure>

## Start

In this article I'm going to use React and [stitches](https://stitches.dev/) (I am [still](https://dev.to/lexswed/choosing-the-right-tools-for-react-component-library-in-2021-1b6g) 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:
```ts
import { styled } from '@stitches/react'

export const Flex = styled('div', {
  display: 'flex',
})
```
_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](https://stitches.dev/docs/installation#configure-stitches) for consistent layout properties, colors, font sizes, etc._

### Wrapping

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

```ts
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',
      },
    },
  },
})
```
_I'm using [`stitches` variants](https://stitches.dev/docs/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:

```tsx
// Flex defined as before

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

export const Column = styled(Flex, {
  flexDirection: 'column'
})
```

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):

```tsx
// 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'
      }
    }
  }
})
```

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

```tsx
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',
      },
    },
  },
})
```

As you [might know](https://developer.mozilla.org/en-US/docs/Web/CSS/flex-flow), `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):

```tsx
<Flex flow="row-reverse" wrap="wrap" />
<Flex flow="column" />
// or with dedicated components
<Row reverse wrap="wrap" />
<Column />
```

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](https://stitches.dev/docs/responsive-styles):

```tsx
<Flex flow={{ '@tablet': 'row', '@mobile': 'column' }} />
```

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](https://css-tricks.com/snippets/css/a-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](https://api.flutter.dev/flutter/widgets/Flex-class.html). It has these two awesome attributes: [mainAxisAlignment](https://api.flutter.dev/flutter/widgets/Flex/mainAxisAlignment.html) and [crossAxisAlignment](https://api.flutter.dev/flutter/widgets/Flex/crossAxisAlignment.html) and the usage is:

```dart
Flex(
  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  crossAxisAlignment: CrossAxisAlignment.end,
)
```

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.

<figure>
<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qe87m4wasf98k9vrxd5f.png" alt="flex main axises for row and column" />
<figcaption>CSS-tricks: Directions (https://css-tricks.com/snippets/css/a-guide-to-flexbox/#flex-direction)</figcaption>
</figure>
 
Knowing this, we can incorporate new API into our own component:

```tsx
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',
      },
    },
  },
})
```

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:

```tsx
<Flex flow="column" main="space-between" cross="center" />
```

![flex-column example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hvaibv7dmau41977fj1v.png)

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](https://developer.chrome.com/blog/new-in-devtools-90/#flexbox) 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](https://github.com/seek-oss/braid-design-system/blob/cdde4b5ab470f138fceaf09a2c4f515b7dc2121e/lib/components/Inline/Inline.tsx#L66) which wraps every children into box with negative margins to allow proper wrapping behaviour without changing the child nodes styles, and [flex-gap-polyfill](https://limitlessloop.github.io/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](https://css-tricks.com/safari-14-1-adds-support-for-flexbox-gaps/). Thankfully, Apple is quite pushing in regards to updates, so we can see [global browser support](https://caniuse.com/flexbox-gap) growing pretty fast.

```tsx
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',
      },
    },
  },
})
```

Few things to comment here. First, you can still use pollyfilled option here, see [an example](https://codesandbox.io/s/stitches-ci-stack-uqj90?file=/src/stack.ts) from an awesome [Joe Bell](https://twitter.com/joebell_). Second, use `xs`, `sm`, etc tokens only if they are already incorporated in your Design System, otherwise, you may consider [`TailwindCSS` number-tokens](https://tailwindcss.com/docs/gap) instead. Third, we don't implement powerful [row-gap](https://developer.mozilla.org/en-US/docs/Web/CSS/row-gap) and [column-gap](https://developer.mozilla.org/en-US/docs/Web/CSS/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](https://stitches.dev/docs/responsive-styles): `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](https://fxtrot-ui.vercel.app/components/Flex). 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.

{% collapsible Full example %}
```tsx
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',
      },
    },
  },
})
```
{% endcollapsible %}

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