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.
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',
})
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',
},
},
},
})
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'
})
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'
}
}
}
})
... 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',
},
},
},
})
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 />
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' }} />
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,
)
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.
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',
},
},
},
})
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" />
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',
},
},
},
})
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',
},
},
},
})
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
Top comments (0)