If your app uses a component based library/framework like React, Vue, or Svelte, leverage the power of re-usable layout components. Go a level higher in the abstraction ladder.
In this article, I'll show you a system of solid layout components with their features and API. These are common and versatile enough to be used for most products and teams. I promise you once you meet these layout, you'll start seeing them everywhere.
In the end, there's a CodeSandbox that uses all these components to build a real use case: a responsive listing of product cards.
I'd like to credit upfront the excellent Braid Design System by Seek-OSS. All the components are heavily inspired from it. You should definitely check it out.
1. Stack
The most common way to layout your interface elements is a vertical Stack. They are everywhere.
<Stack space="large">
<Placeholder height={48} />
<Placeholder height={48} />
<Placeholder height={48} />
</Stack>
API
type StackProps = {
// The element used for the root node.
as?: 'div' | 'span' | 'ol' | 'ul' // Feel free to change these as per your uses
// Defines the spacing between the items.
space?: 'small' | 'medium' | 'large' // All of your spacing tokens
// Sets the horizontal alignment of the items.
align?: 'start' | 'center' | 'end' | 'stretch'
// The items to layout in the stack.
children: React.ReactNode
}
2. Columns and Column
The next most common layout is a single row of Columns.
<Columns space="medium">
<Column><Placeholder height={60} /></Column>
<Column><Placeholder height={60} /></Column>
</Columns>
Collapsing
A common need is to collapse the columns to a stack below a certain size. This can be implemented using ResizeObserver
(and hopefully soon using Container Queries).
Alignments
Columns support alignments in both directions.
Individual column sizing
We have a Column component to wrap each column that provides ability to size itself in different implicit and explicit sizes.
<Stack space="medium">
<Columns space="xsmall">
<Column width="content">
<Placeholder height={30} label="content" />
</Column>
<Column>
<Placeholder height={30} label="fluid" />
</Column>
</Columns>
<Columns space="xsmall">
<Column width="1/5">
<Placeholder height={30} label="1/5" />
</Column>
<Column>
<Placeholder height={30} label="fluid" />
</Column>
</Columns>
<Columns space="xsmall">
<Column width="1/4">
<Placeholder height={30} label="1/4" />
</Column>
<Column>
<Placeholder height={30} label="fluid" />
</Column>
</Columns>
<Columns space="xsmall">
<Column width="1/3">
<Placeholder height={30} label="1/3" />
</Column>
<Column>
<Placeholder height={30} label="fluid" />
</Column>
</Columns>
...
</Stack>
API
type ColumnsProps = {
// The element used for the root node.
as?: 'div' | 'span' | 'ol' | 'ul'
// Defines the spacing between the items.
space?: 'small' | 'medium' | 'large' // All of your spacing tokens
// Collapse items to a stack below this size.
collapseBelow?: number
// Sets the horizontal alignment of the items.
align?: 'start' | 'center' | 'end' | 'stretch'
// Sets the vertical alignment of the items.
alignY?: 'start' | 'center' | 'end'
// The columns.
children: React.ReactNode
}
Cluster
This is very similar to Columns but it allows the items to wrap if needed. Most commonly used to show a list of badges.
<Cluster space="small">
<Tag tone="positive" weight="subtle" label="Confirmed" icon="IconCalendar" />
<Tag tone="cautional" weight="subtle" label="Pending" icon="IconApple" />
<Tag tone="informational" weight="subtle" label="Hired" icon="IconRemote" />
<Tag tone="neutral" weight="subtle" label="Refunded" icon="IconLike" />
<Tag tone="promotional" weight="subtle" label="New" icon="IconHeart" />
</Cluster>
Clusters also support collapsing and alignments in both directions just like Columns.
API
type ClusterProps = {
// The element used for the root node.
as?: 'div' | 'span' | 'ol' | 'ul'
// Defines the spacing between the items.
space?: 'small' | 'medium' | 'large'
// Collapse items to a stack below this size.
collapseBelow?: number
// Sets the horizontal alignment of the items.
align?: 'start' | 'center' | 'end'
// Sets the vertical alignment of the items.
alignY?: 'start' | 'center' | 'end'
// The items to lay out in this cluster.
children: React.ReactNode
}
AutoGrid
Auto grid is basically a component abstraction for repeat-auto-minmax.
<AutoGrid space="medium" minItemWidth={120}>
<Placeholder />
<Placeholder />
<Placeholder />
<Placeholder />
<Placeholder />
<Placeholder />
<Placeholder />
</AutoGrid>
API
type AutoGridProps = {
// The element used for the root node.
as?: 'div' | 'section' | 'ul' | 'ol'
// The minimum width for items to shrink to before the grid starts wrapping to make space.
minItemWidth: number
// The gap between the items or the column gap if spaceY is present.
space?: 'small' | 'medium' | 'large'
// The row gap between the items.
spaceY?: 'small' | 'medium' | 'large'
// Items inside the AutoGrid.
children: React.ReactNode
}
Layout Components In Action
Now that we've seen the components. Let's see them in action.
I've used NextJS and Vanilla-Extract to style the components. But you can, of course, use any solution down to plain CSS.
Let's appreciate all the functionality going on in the demo:
- We have a sidebar that collapses on smaller screens.
- The product grid is responsive
- The card itself has two layouts: horizontal and vertical and it responds based on available space.
- The badges inside the Cluster can wrap if required.
- The buttons also respond to available size and collapse if too narrow.
All this with just a system of well thought out components. Totally declarative, readable, and re-usable.
And we've only covered the most basic common denominator of layout components. You can do much more depending on your use case. Like:
- Add a Spacer component to create flexible space between Stack or Column items.
- Add a prop to make column items fill the available space inside the Column.
- Add a FixedGrid with explicit column and row counts to create a rigid grid.
But I hope that you find this system inspiring and already have ideas to build on top of this. Feel free to fork the sandbox or the repo to use it as a starting point.
I'll see you next time! 👋🏻
Top comments (3)
Anyone who is trying to make as much of their layout code use only CSS rather than CSS and JavaScript might be interested to know that the Collapsing component here can be built with CSS only, no
ResizeObserver
required!Here's an example: every-layout.dev/demos/switcher-basic . The example is taken from a very good ebook called Every Layout: every-layout.dev/layouts/switcher.
heydonworks.com/article/the-flexbo... is a free-to-all blog post that describes the same technique, just in an older version. The blog post uses
margin
to add spacing, but the Every Layout version usesgap
now that browsers supportgap
in Flexbox layouts.That’s right. I initially tried to use the “Switcher” CSS but don’t remember why I had to abandon it. Might be worth going back a retry.
Also, EveryLayout.dev is an excellent resource and worth getting a copy. The name “Cluster” comes right from it too. 😄
I thought there some similarity around your shared use of the name 'Cluster' but wasn't sure if that was a coincidence or not! 😄