souporserious

Stack Components with CSS Grid

7 min read

Grid is praised for its two-dimensional layout capabilities but gets overshadowed by Flexbox for one-dimensional layouts. In this post, we'll look at how we can unlock advance layouts using components.

In design, space is considered a first-class element. One of my favorite quotes is from the famous graphic designer Jan Tschichold:

White space is to be regarded as an active element, not a passive background.

Space is often an afterthought in web development. When hearing the word spacing, you may immediately think of padding or margin. Recently we’ve seen more importance around space with properties like gap, but for now, we can only practically use this with Grid, while Flexbox is left to wait for better support or use a polyfill. Even then, these still lack priority for more advanced properties like minimum and maximum constraints or fractional units.

What we'll be building

In the previous article, we talked about how Stack components are a better paradigm for one-dimensional layouts, and we briefly discussed a tool called Subform that took a new approach to layout. While only being on the scene for a short time, the paradigms created in this tool greatly simplified how we think about positioning items in relation to each other.

We’ll be using an inspired API to control the spacing between and around children on our main axis. Follow along with this example to learn how we can build more advanced one-dimensional layouts:

The power of components

With components, we can encapsulate our styling logic in a first-class API, not have to wait for better support, and unlock more advanced ways to style our applications.

One of the cons mentioned in the Subform implementation was having to ship a custom layout engine. This is a good reason not to have to load extra code, especially Javascript, just to render something that looks like Flexbox. However, with component models being standard in today’s development workflows, we can take an interface similar to Subform and map it to whatever we want. While Houdini is still a ways off from having proper browser support for custom layouts, CSS Grid has the power we need to accomplish most of the same ideas. Instead of being able to control space on both our main and cross axes, we will only be dealing with the main axis today.

Getting the API right

We’ll start with the interface for controlling styles through our component:

1interface StackProps {
2 axis?: 'horizontal' | 'vertical'
3 width?: Number | String
4 height?: Number | String
5 alignment?: 'start' | 'center' | 'end'
6 spaceStart?: Number | String
7 spaceBetween?: Number | String
8 spaceEnd?: Number | String
9 background?: String
10 foreground?: String
11 children: React.ReactNode
12}

Only three of our props specifically deal with space. Notice we don’t have margin or padding defined. Thinking about space as an element lends itself to an extremely simplified way of describing layout and, at the same time, unlocks the full constraint system that width and height use. Another problem this solves is that the gap property doesn’t have a way to use dividers, colors, or backgrounds in gaps. Yet another reason to think of space as an element.

Like Kevin mentions in his article, this dramatically simplifies our layout API. Concepts are consolidated and easy to learn. How many times did you need to look up documentation for Flexbox or Grid?

Again, the most significant benefit of this compared to current spacing techniques through padding, margin, or gap are we can use the same constraints we use for other elements like minmax, fr units, and anything else an element can use.

Grid to the rescue

Since Grid implements parent-controlled layout, we can map through children in our component to extract the proper information to build up our layout to suffice the API we want:

1const isHorizontal = axis === 'horizontal'
2const trackCells = React.Children.toArray(children).reduce(
3 (cells, element, index) => {
4 const cell = {
5 element,
6 size: React.isValidElement(element)
7 ? (isHorizontal ? element.props.width : element.props.height) || 'auto'
8 : null,
9 }
10 return index === 0 || spaceBetween === undefined
11 ? cells.concat(cell)
12 : [...cells, { size: spaceBetween }, cell]
13 },
14 []
15)
16if (spaceStart) {
17 trackCells.unshift({ size: spaceStart })
18}
19if (spaceEnd) {
20 trackCells.push({ size: spaceEnd })
21}

First, we determine the direction of our main axis. Next, we loop through all children to determine what size each column or row should be. If the child has not defined a size, then we set it to auto. While looping through, if we have set spaceBetween we will insert that information in between each child while we build up our array. Finally, we add spaceStart and spaceEnd to their respective positions in the array if they are defined.

This gives us a nice collection to work with similar to the following:

1const trackCells = [
2 { size: 'minmax(20px, 1fr)' },
3 { element: { $$typeof: Symbol(react.element) }, size: 'minmax(100px, 1fr)' },
4 { size: '20px' },
5 { element: { $$typeof: Symbol(react.element) }, size: 'minmax(100px, 1fr)' },
6 { size: 'minmax(20px, 1fr)' },
7]

Now that we have the proper information for the constraints of our layout we can build up our Grid styles, notice we create columns/rows that are specifically used for space:

1trackCells
2 .map((cell) => (typeof cell.size === 'number' ? `${cell.size}px` : cell.size))
3 .join(' ')
4
5// minmax(20px, 1fr) minmax(100px, 1fr) 20px minmax(100px, 1fr) minmax(20px, 1fr)

Since we are creating implicit columns/rows solely for spacing, we need to position the children accordingly. This is a nuanced problem and unfortunately forces the children to be aware of the implementation. We’ll use React Context to pass this information down:

1trackCells.map((cell, index) => (
2 <StackContext.Provider
3 value={{
4 [`grid${isHorizontal ? 'Column' : 'Row'}`]: index + 1,
5 }}
6 >
7 {cell.element}
8 </StackContext.Provider>
9))

Put it all together, and we have a Stack component that can define advance spacing constraints:

1const StackContext = React.createContext(null)
2
3function Stack({
4 axis,
5 width,
6 height,
7 alignment,
8 spaceStart,
9 spaceBetween,
10 spaceEnd,
11 background,
12 foreground,
13 children,
14 ...restProps
15}) {
16 const isHorizontal = axis === 'horizontal'
17 const cellStyles = React.useContext(StackContext)
18 const trackCells = React.Children.toArray(children).reduce(
19 (cells, element, index) => {
20 const cell = {
21 element,
22 size: React.isValidElement(element)
23 ? (isHorizontal ? element.props.width : element.props.height) ||
24 'auto'
25 : null,
26 }
27 return index === 0 || spaceBetween === undefined
28 ? cells.concat(cell)
29 : [...cells, { size: spaceBetween }, cell]
30 },
31 []
32 )
33 if (spaceStart) {
34 trackCells.unshift({ size: spaceStart })
35 }
36 if (spaceEnd) {
37 trackCells.push({ size: spaceEnd })
38 }
39 return (
40 <div
41 style={{
42 display: 'grid',
43 gridAutoFlow: axis === 'horizontal' ? 'column' : 'row',
44 [`gridTemplate${isHorizontal ? 'Columns' : 'Rows'}`]: trackCells
45 .map((cell) =>
46 typeof cell.size === 'number' ? `${cell.size}px` : cell.size
47 )
48 .join(' '),
49 [`${isHorizontal ? 'align' : 'justify'}Items`]: alignment,
50 color: foreground,
51 background,
52 width,
53 height,
54 ...cellStyles,
55 }}
56 {...restProps}
57 >
58 {trackCells.map((cell, index) => (
59 <StackContext.Provider
60 value={{
61 [`grid${isHorizontal ? 'Column' : 'Row'}`]: index + 1,
62 }}
63 >
64 {cell.element}
65 </StackContext.Provider>
66 ))}
67 </div>
68 )
69}

Summary

While this solution might not be entirely practical yet, simplifying how we approach styling not just on the web, but throughout the industry lowers the barrier to entry for more people to be able to express themselves. If we can consolidate API surfaces and reduce nuances, we can think about more significant problems at hand, like user experience and accessibility. If anything, I hope this article can help shed light on how to start thinking of space as a first-class element in your design and development endeavors.

Updated:
  • design
  • development
  • css
  • grid
  • flexbox
  • react
Previous post
Easier Layouts with Stacks
Next post
Build a Simple FLIP Animation in React
© 2022 Travis Arnold