(This article originally appeared on Medium)
If you're a software engineer, then like me, you spend a lot of your time reading and writing code. The more code you read, the more patterns you begin to observe. The more code you write, the more you understand why one pattern may be chosen over another. But some patterns are better than others, and some are downright terrible.
A Simple Design Taken Entirely the Wrong Way
Imagine you are given designs for a new website layout, and need to come up with a PageLayout
component that implements the design. According to the specs, the finished page should look something like this on desktop:
+--------------------------------------------------------+
| Logo Settings |
+-------------------------------+------------------------+
| | Page Title |
| +---------+--------------+
| Main | Nav | Page |
| Page | Links | Description |
| Content | ... | ... |
| . +---------+--------------+
| . | |
| . | (empty) |
| . | |
| +---------+ |
| | Share | |
| | Buttons | |
+-------------------------------+---------+--------------+
Ok, a bit of an odd layout, but at first glance it looks pretty easy to implement:
- Put a full-width
div
going across the top of the page - float the logo left, user settings right - Set up 2 main columns - one on the left (for scrollable page content), and one on the right (for...everything else)
- Hmm... the site title, links, and description are arranged in the same way the overall page is! Let's just reuse our
PageLayout
component - we'll keep it DRY!!!- Edge case: We just need to be sure we never pass a
PageLayout
instance as the page description, or the component render may go into infinite recursion and break the site, likely making it difficult for the user to close the window.
- Edge case: We just need to be sure we never pass a
- Phew, glad that's over with... what about these share buttons? With nothing else around them? Let's just add
position: absolute;
and call it a day.
Yeesh... not as simple as it looked initially. But, hours were spent on it, so time to check the code into version control!
A Pull Request that Shouldn't Have Been Approved
Let's quicky go over what's been done so far:
Does the above approach produce the desired layout? Yes, if we make certain assumptions about how the CSS is organized (hint: generally not safe to do).
Is there any ambiguity as to where the child elements will render? No, except for maybe the share buttons, but we can just have consumers override the styles if necessary.
Does the component demonstrate any useful abstractions? Well, the outer layout was reused as part of the right column, so it saved the developer from writing a few extra div
elements...
Ok, overall could be better, but it exhibits the necessary and sufficient conditions for the given design - which, if you ask a logician, is all it takes for something to be considered acceptable or correct.
So, what's the problem?
A Sudden Shift in Perspective Changes Everything
Once the new page hits production, everyone loves it. The marketing team is seeing great numbers following your deploy, and initial readings from various SaaS analytics platforms indicate that users are receptive to the redesign and are becoming more engaged.
Given all this success, the design team decides it's time to take it to the next level. What does the next level look like? Pretty much the same, but now also on mobile:
+-----------------------------------------+
| Logo Page Title Settings |
+-----------------------------------------+
| |
| |
| |
| |
| |
| |
| |
| Main |
| Page +---+
| Content | S |
| . | h |
| . | a |
| . | r |
| . | e |
| +---+
| |
| |
| |
+-----------------------------------------+
| |
| Page Description |
| |
+-----------------------------------------+
| N a v L i n k s |
+-----------------------------------------+
Ok, looks really clean... but nothing like the original design - the nav moved to the footer, the share buttons are in a totally different place, the columns are gone!! How am I supposed to translate what I just did into that? 😱
From here, I see pretty much 3 potential paths forward:
- Add a bunch of conditional logic to reposition elements
- Have 2 separate implementations - one for desktop, one for mobile
- Redesign to better accommodate both layouts
Option 3 sounds like a ton of work... let's get back to that one later.
How about Option 1? To rearrange the desktop layout into the second would likely spell disaster, and possibly even certain death for your new favorite component.
A ton of CSS rules and class names would have to be adjusted, covering pretty much every child element on the page (except for perhaps the header row containing the logo and settings menu). Looks like we could keep the position: absolute
for the share buttons, but that won't count for much.
Using a CSS framework like Bootstrap to implement your layout? The entire point of such things is to make weird styling rules like these unnecessary, so even then it would just not feel right to get away by only mangling the existing layout.
So Option 1 is out - what about Option 2? Sure, that will be easier initially, but in the long run, no one is going to want to support two completely separate implementations of the same thing. Granted, this does happen all the time in the real world, and is often a good choice for enterprise-scale teams. Still, the whole team at Podible fits into a single room at our NYC WeWork office, so naturally, we prefer to avoid such scenarios when possible.
Which brings us back to Option 3.
A Second Look at an Old Problem
Let's revisit the two layouts - both desktop and mobile pages have a box-like structure, but the contents seem to be in totally different places depending on the type of viewing device. What are some possible ways we could redo the PageLayout
component to suit both arrangements?
- Separate 3rd-party template - less up-front work, but what happens when you need to make an adjustment that is hidden deep inside the template? OSS contributions are always valued, but if the project is not well-maintained, you could be led down a deep rabbit hole.
- Bootstrap - Responsive, but still would require ample help from JS
- Flexbox - Getting warmer, but would result in a bunch of extra div elements that act only as containers for the actual content
- CSS Grid - there seems to be a lot of talk about CSS grid nowadays. Let's see what the hype is all about!
Before diving in and coding up the thing, it will serve us well to first look at a reduced example, to see how a simple layout can be implemented with CSS grid. One of my favorite ways is with grid-template-areas
and grid-area
- these CSS rules allow you to create pretty wacky layouts with relative ease:
.wacky-grid {
display: grid;
grid-template-areas:
". . area-1"
". area-2 . ";
}
.top-right {
grid-area: area-1;
}
.bottom-center {
grid-area: area-2;
}
So the .wacky-grid
container will become a 2x3 grid with two named grid areas area-1
and area-2
. A grid area is a rectangular region of cells that can be specified either by name or with row & column start-/end-points.
A key concept of CSS grid is that you can place its contents basically wherever you want. Under a .wacky-grid
element, children with the top-right
class name will be placed in the area-1
grid area, and children with the bottom-center
class name will be placed in the area-2
grid area. Cells marked with a .
will stay empty. As for the whitespace, I just added it for readability - the rule could be equivalently written as:
grid-template-areas: ". . area-1" ". area-2 .";
...but that makes it a lot less clear what the end result will look like. 😉
An implementation of WackyGrid
might look something like this:
const WackyGrid = () => (
<div className="wacky-grid">
<div className="top-right">
I'm up in the corner!
</div>
<div className="bottom-center">
I'm down in the middle!
</div>
</div>
);
Look ma, no container div
s! 😁 Each child gets placed in the appropriate cell. Bonus: if you have additional children without any grid-specific CSS rules, they will get automatically placed in the leftover cells.
Note that grid cells can't be styled directly - only actual HTML elements can. So, if you have an empty cell you want colored black, you'll still need a div to put there.
Ok, now it's time to get down to business...
Branching Layouts With Ease
If CSS grids can teach you anything, it's that you can likely stop worrying so much about the HTML structure of a document, and start giving more consideration as to how the more critical pieces of data can be arranged in an intelligent way. You don't need to add a wrapper element every time you want to have a row inside a column.
⚠️ DISCLAIMER ⚠️
Before you go back to your team and say, "this article said we should use CSS grid for everything!!!", take a step back and think about when you would prefer CSS grid over another tool such as a flexbox or a 3rd-party library/framework. I find that this image sums things up fairly well:
With that PSA aside, let's specify a grid for the initial layout:
PageLayout.scss
.page-layout {
display: grid;
grid-template-areas:
"header header header"
"main title title"
"main nav description"
"main share .";
}
Nice! Super simple. But how do we differentiate desktop from mobile layouts? The grid-tempate-areas
rule is perfect for describing such subtleties:
.page-layout {
display: grid;
}
.page-layout-desktop {
grid-template-areas:
"header header header"
"main title title"
"main nav description"
"main share .";
}
.page-layout-mobile {
grid-template-areas:
"header"
"main"
"description"
"nav";
}
Alright, this is starting to make sense. Still, there are some fuzzy bits - we can see very clearly that the grid area names don't match up perfectly. In particular, the share
grid area is missing from the mobile layout!
(If it weren't for the share buttons, I might say it'd be easier to just use a flexbox on mobile. For the sake of the blog post, though, let's stick to the fun new stuff 🙃)
We can address these ambiguities by mapping our elements and class names to grid areas:
// applies regardless of device type
.page-layout {
.logo, .user-settings {
grid-area: header;
}
.logo {
justify-self: start;
}
.user-settings {
justify-self: end;
}
nav {
grid-area: nav;
}
main {
grid-area: main;
}
.description {
grid-area: description;
}
}
// desktop-specific placement
.page-layout-desktop {
.page-title {
grid-area: title;
}
.share-buttons {
grid-area: share;
align-self: end;
}
}
// mobile-specific placement
.page-layout-mobile {
.page-title {
grid-area: header;
justify-self: center;
}
.share-buttons {
grid-area: main;
align-self: center;
justify-self: right;
}
}
This part is a bit more verbose - and there are definitely other ways of placing child elements - but this gives me a pretty good idea of where each element will appear on the page. But there's one last piece that will drastically affect how the grid appears in its final form:
.page-layout-desktop {
grid-template-rows: 1fr 1fr 25vh auto;
grid-template-columns: 5fr 2fr 3fr;
}
.page-layout-mobile {
grid-template-rows: 1fr auto 2fr 1fr;
// not necessary to specify columns, since there's only one
}
All it needed was some good ol' row & column sizing - without these rules, the 3 desktop rows/columns would all render with the same height/width, and the mobile main area would only take up 1/4 of the screen.
If you’re wondering about this mysterious fr
unit, it’s short for fraction, and is specific to CSS grid. Although you can implement your grid layout using only traditional units like %
, em
or vh
/vw
, I'd say it's probably the most useful unit for CSS grids, as it helps to eliminate some unnecessary calculations, e.g. repeat(4, 1fr)
vs repeat(4, 25%)
. Still, it’s an arbitrary choice - use whichever units make the most sense for your use case (but beware of px
!).
Finally, we'll implement the PageLayout
component by simply listing the page contents in a sensible, layout-independent order (child props omitted for brevity):
PageLayout.jsx
const PageLayout = ({ type = 'mobile' }) => (
<div className={`page-layout page-layout-${type}`}>
<Logo />
<NavLinks />
<PageTitle />
<PageDescription />
<Main />
<ShareButtons />
<UserSettings />
</div>
);
Now, our stylesheet will take all the child elements and place them into the corresponding grid areas. There, that wasn't so hard! 😄
Ok, but why?
Now is about the time where you might find yourself saying, "this seems neat, but can I do anything useful with this besides moving elements into strange places?"
You sure can.
At Podible, we believe in always using the right tool for job, and CSS grids efficiently solve a number of problems that are only partially addressed by most 3rd-party tools. And the more we experiment with grid layouts, the more areas it seems they can prove useful. Take our latest addition for example - the embeddable player:
Right now it only makes minor adjustments according to its width, but the setup is such that we'll be able to easily add new layouts to this component in the future.
That's all for now - let me know how you plan to use CSS grid next in the comments below. Thanks for reading!
Want to join in on the action? Podible is hiring a full-stack engineer!
Top comments (2)
Java's AWT layout managers already invented this ... like 20 years ago. Not kidding!
CSS Grid is great but has a poor support in IE11: caniuse.com/#feat=css-grid
So for those who still must support IE11 the flexbox is the only stable solution for now 😔