Table of contents
A few thoughtful layout components can go a surprisingly long way and help you lay out pages faster.
I’m currently having my brain rewired by Every Layout, the online book/course/thing from Heydon Pickering and Andy Bell. It describes how a few thoughtful, robust layout components that use shockingly little code with modern CSS can be combined to build nearly, well, every layout. It’s worth the money, but you can read about the Stack, the Cover, and the Sidebar components for free.
The Sidebar, in particular, is a revelation to me. You end up with a container that will put two child elements side-by-side, but also you can specify that one element should have an “ideal” width with flex-basis
and the other element should take up any remaining space but if that remaining element is now taking up less than half the total space, both elements will collapse into a single-column layout and grow to the full width of the container and this stuff used to be weirdly hard but now it’s like 10 lines of CSS and you don’t even need media queries or container queries.
🤯
Anyway, while I’m picking up my little brain bits, I thought I’d share some of the layout components that I built at my last job that I found particularly useful to have in my toolkit.
Once built and refined, I used these all the time and they saved a ton of effort and code. At my old job I used React, but here I’ll link out to some implementations that reflect how I would build them today with Svelte. These would also be very doable with some CSS classes and maybe CSS properties and calc()
for on-the-fly math, so don’t feel like these are out of reach if you don’t specifically use Svelte.
The Stack
It sucks to think about margins.
Like, even on the smallest scale, it’s awful: you have two things and you want some vertical space between them. Should you put margin-bottom
on the top one? Or would it be better to put margin-top
on the bottom one? What if you add more things? What if you change the order of things and now have to muck with the margins again? What if you remove one of the things and now there’s extra margins that you don’t need? Aw geez I haven’t gotten to the fact that vertical margins collapse OMG.
It doesn’t have to be complicated: set up a wrapper component that inserts space between things for you and stop thinking about margin ever again.
In use, it’ll look like this:
<Stack gap="1rem">
<SomeThing />
<SomethingElse />
<AnotherThing />
</Stack>
What the <Stack>
does is insert the desired gap
amount of space between all the direct child elements. That’s all! You’ll use this all the time! Wrap an input and a label and some helper text and put a tiny space between them; insert a big honkin’ space between unrelated things; wrap your main text markup to insert some space between whatever children paragraphs or lists or images or what-have-you.
You might find it useful to restrict the possible values on the gap
property to maybe specific spacing values in your design system, or you might do some math in JavaScript or with calc()
. But inserting some space between children is the meat of it.
I think I heard about this concept first from an article cheekily titled “Margin considered harmful”, which pointed to a Stack
component in the Braid design system.
When I made my own for work, it was coincidentally nearly identical to The Stack in Every Layout, so go check that out. Like that one, I handled the spacing with a * + *
lobotomized owl selector and (ugh) margin-top
between the children, but today flexbox gap
is well-supported so I’d use that.
Here’s a variation that uses flex gap
and calc()
as a Svelte component. I have it set up so the gap space is based on 1rem
, so the spacing scales with root text size. It has some real-world quality-of-life niceties like accepting a class
from its parent in case you need to apply some other styles and a fullWidth
property that can sometimes get you out of a jam if you’re using a <Stack />
as a child of some other flex/grid bits and it’s shrinking more than you’d like.
The Row
The <Row />
is the horizontal/inline version of the <Stack />
.
A component that puts things side-by-side seems like a “big whoop” but, well, you kinda do that a lot. Nicole Sullivan pointed out more than a decade ago that Facebook’s entire news feed at the time was a thing next to another thing times a zillion and gave name to the “Media object” pattern.
This works for both big and little tasks: put an icon next to some text; put an avatar next to a dialog box; align a clump of form buttons; put a pile of menu items in a line, make a quick-and-dirty sidebar + main layout.
<Row />
handles inline spacing between items for you with a gap
property because thinking about horizontal margin also sucks. It also has some other quality-of-life niceties:
- It uses flexbox for the layout, which means you mostly don’t have to think about if the children are inline or block-level elements because that doesn’t matter so much when they’re flex children.
- It exposes flex
align-items
values (egalign="center"
) to control vertical alignment of the children. - It has a
splitLast
property that shoved the last element to the far end of the line, because that came up a lot in our designs. Your requirements may differ but this meant I didn’t have to also expose thejustify-content
properties. - It has optional
wrap
(addsflex-wrap: wrap
) andoverflow
(addsoverflow-x: scroll
) properties as escape hatches when there were more items than could fit in a single line.
A nice bonus from using gap
for spacing is you also get vertical spacing between items if they wrap into multiple lines. You can do all that with ugh margin
s but you have to do some math and use negative margins on the container to correctly handle wrapping and not waste space. Flex gap
is simpler and clearer.
In use it looked like this, say, in a page’s header:
<header>
<Row gap="1rem" align="center" splitLast>
<Logo />
<UserMenu />
</Row>
<Row wrap gap="0.5rem">
<a>Menu item</a>
<a>Menu item</a>
<a>Menu item</a>
<a>Menu item</a>
<!-- etc -->
</Row>
</header>
Or small-scale sticking an icon next to some text:
<button>
<Row align="center" gap=".25rem">
<SomeSvgIcon />
<span>A label</span>
</Row>
</button>
Here’s an implementation as a Svelte component that uses flex gap
and calc()
for the spacing.
The Space
Once you’ve built <Stack />
and <Row />
, you find uses for them everywhere. However, your designer will inevitably throw you an entirely predictable curveball with a bunch of different spaces in between things. Like they’re using whitespace meaningfully and helpfully instead of just wanting the exact same amount of space between everything ugh.
One way to get mix-and-match spacing is to nest some of our fancy components, but that looks gnarly real fast:
<Stack gap="4rem">
<h1>Page title!</h1>
<!-- ok 4rem space between the title and the below -->
<form>
<Stack gap="2rem">
<Stack gap="1rem">
<Stack gap=".25rem">
<label>Label</label>
<!-- 0.25rem space between these bits -->
<input type="text" />
</Stack>
<!-- 1rem space between these Stacks -->
<Stack gap=".25rem">
<label>Label 2</label>
<!-- 0.25rem space between these bits -->
<input type="text" />
</Stack>
</Stack>
<!-- 2rem space between the above Stack and below div -->
<div>
<button type="submit">Submit</button>
</div>
</Stack>
</form>
</Stack>
Alright that could be much worse but with a big page and a bunch of different components it gets hard to keep track in the code how much space should be between things.
So when that got annoying or illegible, I’d reach for a simple component that I called <Space />
, which is just an empty box that you could assign a height and width to.
So I’d probably unwrap a good chunk of the above like so:
<h1>Page title!</h1>
<Space h="4rem" />
<Stack gap=".25rem">
<label>Label</label>
<input type="text" />
</Stack>
<Space h="1rem" />
<Stack gap=".25rem">
<label>Label 2</label>
<input type="text" />
</Stack>
<Space h="2rem" />
<div>
<button type="submit">Submit</button>
</div>
…and that feels a lot nicer to me. It’s easy to scan and trivially easy to tinker with the spaces by just playing with the h
properties.
<Space />
is also super-handy when you need, well something that took up some space. Maybe you needed some padding on the bottom of something but it wasn’t super clear on which element you would want to apply margin/padding or which of margin/padding was more appropriate. You don’t worry about that; chuck in a <Space />
and go for a pleasant walk.
The Svelte implementation of <Row />
that I linked to above has a simple <Space />
component that, like the others, uses calc()
and CSS properties for the sizing, but, again, your own implementation might use some other system or mathematics for defining what size a typical “space” is.
Putting it all together
As a demonstration of how useful these three components are, here’s like a whole blog article page put together almost entirely with <Row />
, <Stack />
and <Space />
.
There’s a little checkbox floating about that you can hit to show the outlines of the various components, for the curious:
Obviously do not really do this! Like, I’m using <Space />
in places where I should probably put some padding
and I probably would do something more interesting in the header. And, ok, sure, I had to apply flex-grow: 1
a few places to make a few things expand where they ordinarily wouldn’t.
But this is a perfectly cromulent full page layout that doesn’t embiggen your CSS, it’s (mostly) responsive and functional on phones or big screens with zero media queries, it’s easy to fuss with alignment and spacing right there in the markup, and it came together in just a few minutes. That’s pretty neat!
If you’re looking at this and are all “gross why so many wrappers and unnecessary bits of markup", well… I do admire your sense of purity but have fun with margins!