Creating modular layouts in React, part 2
To get started with this post, you can pull up the code from part 1 or you can grab it from Github. As an aside, I highly recommend viewing the first article to help provide the necessary context. Moving right along, if you decided to clone the repo, open it in your code editor and run the following in the terminal:
cd part-2/tutorial/startyarn installyarn start
These commands will shift you into the start
directory and install all of the necessary dependencies for this project. Additionally, you can view the final code for this tutorial in the finish
directory.
Once you run yarn start
, you should see the following in your browser's localhost:3000
:
Okay, let’s get started!
Custom Content Positioning
For most sites, you may not need the ability to customize the positioning of content. But in cases, like my portfolio site, you may want to create a more organic look and feel for your site.
So, to accomplish custom content positioning, we will need to update our Column
component in two ways:
- to use
grid-column-start
in our desktop view. - to inherit a
columnStart
prop from eachColumn
instance.
To get started, let’s open Column/index.js
in our code editor. We will add our grid-column-start
property to our desktop media query, and have it consume a prop called columnStart
. Our updated styled component will look like this:
// src/Column.jsconst Column = styled.div`grid-column-end: -1;${mqs[0]} {${tabletColumns()};}${mqs[1]} {${desktopColumns()};grid-column-start: ${(props) => props.columnStart};}margin: 0;`;
Now, let’s create a new component in Sections
called Lookbook.js
. We will use our new custom content positioning technique in here. This setup will have a very similar code structure to our Top
component, but with one essential update: some Column
components will now include the new columnStart
property. We will also be leveraging some of the amazing imagery from Pexels for our component. Here’s my set up:
// src/Sections/Lookbook.jsimport React from "react";import styled from "styled-components";import Column from "../Column";const Container = styled.div`display: flex;flex-flow: column wrap;img {max-width: 100%;object-fit: cover;}font-size: 1.3rem;h3 {font-size: 2.1rem;margin-bottom: 0px;}h4 {font-size: 1.6rem;margin-bottom: 0px;}margin-bottom: 80px;`;const Index = () => {return (<><Column className="m"><Container><img src="https://images.pexels.com/photos/1366919/pexels-photo-1366919.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940" /><h3>Landscape Photography of Snowy Mountain</h3><p>by eberhard grossgasteiger</p></Container></Column><Column className="s" columnStart="9"><Container><img src="https://images.pexels.com/photos/2217365/pexels-photo-2217365.jpeg?auto=compress&cs=tinysrgb&dpr=3&h=750&w=1260" /><h3>Landscape Photo of Riverand Pine Trees</h3><p>by eberhard grossgasteiger</p></Container></Column><Column className="l" columnStart="3"><Container><img src="https://images.pexels.com/photos/808465/pexels-photo-808465.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940" /><h3>Brown Wooden Dock Surrounded With Green Grass</h3><p>by Tyler Lastovich</p></Container></Column><Column className="m"><Container><img src="https://images.pexels.com/photos/1308185/pexels-photo-1308185.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940" /><h3>Hobbit House</h3><p>by Tyler Lastovich</p></Container></Column><Column className="m"><Container><img src="https://images.pexels.com/photos/850672/pexels-photo-850672.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940" /><h3>Brown Cattle</h3><p>by Tyler Lastovich</p></Container></Column><Column className="m" columnStart="3"><Container><img src="https://images.pexels.com/photos/1955134/pexels-photo-1955134.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260" /><h3>Empty Highway Overlooking Mountain</h3><p>by Sebastian Palomino</p></Container></Column></>);};export default Index;
A few things to note here:
- We are wrapping each section with a
Column
component. - Each
Column
component has aclassName
property to determine the width of the section. - Some
Column
components also include acolumnStart
property which allows us to designate where content should start on the grid.
Now, let’s render our Lookbook
component in our App
component. First, import Lookbook
into App.js
and then replace <Top />
with <Lookbook />
:
// src/app.js// ... other imports hereimport Lookbook from "./Sections/Lookbook";function App() {return (<div className="App"><Grid><Lookbook /></Grid></div>);}export default App;
You should now see the following in your locahost
:
Not too shabby! We’ve got a layout that feels organic, has a dynamic and asymmetric flow to it, and is also responsive. We could even call it a day and stop now.
However, there is a problem: as our site scales down, it loses its dynamic nature. Essentially everything goes full width, and we lose the nice spacial flow seen in the desktop version. Now this is fine, even optimal, for mobile given the small real estate. However, it would be nice to maintain this gallery look for tablet. To accomplish this, we need the tablet version to retain the same grid as the desktop version.
Creating new column sizes
To kick this off, navigate over to the src/utils
directory. Duplicate columnSizes.js
and call it galleryColumns.js
. Now, we are going to trim the code in this file down. Remember we want the same column structure for desktop and tablet. This means we only need to keep the desktop column sizes around. So first we will update our columnSizes
object to the following:
// src/utils/galleryColumns.jsconst columnSizes = {xl: {desktop: 12,},l: {desktop: 8,},m: {desktop: 6,},s: {desktop: 4,},xs: {desktop: 3,},xxs: {desktop: 2,},};
Next up, we only need to export one function for desktop sizes. So let’s convert the desktopColumns
function to galleryColumns
, and remove the tabletColumns
function:
// src/utils/galleryColumns.jsexport const galleryColumns = () => {var classes = "";for (let key in columnSizes) {classes += `&.${key} { grid-column-end: span ${columnSizes[key].desktop}}`;}return classes;};
This function will export classes for each of our column sizes that we can use in our gallery grid component.
The Gallery Grid component
I’m about to commit one of the cardinal sins of code: writing duplicate code. Normally, in React, one wants to create components that are flexible, that can render themselves differently based off of certain conditions.
Ideally, I’d like to do that for my Grid component. In this scenario, if I wanted a normal Grid, then I could just use:
<Grid><Column>my content</Column></Grid>
And if I wanted a gallery style grid, then I could just use:
<Grid type="Gallery"><Column>my content</Column></Grid>
One component, multiple variations. However, this ideal outcome would unfortunately be more complicated to achieve. We would have to pass a special prop down to each Column component, to have it conditionally render the correct classes. So it would end up looking something like this:
<Grid type="Gallery"><Column type="Gallery">My content</Column><Column type="Gallery">My content</Column></Grid>
Additionally we would have to write extra conditional logic for both our Grid and Column component.
So, we’re going to avoid a bunch of unmaintainable, spaghetti code and opt to create new, distinct components for our gallery style grid.
To start, create a GalleryGrid
folder in your src
directory. Then create an index.js
file inside of your new folder. Finally, copy and paste the code from your Grid/index.js
file into your GalleryGrid/index.js
file.
All together it should look like this:
// GalleryGrid/index.jsimport React from "react";import styled from "styled-components";import { mqs } from "../utils/breakpoints";const Grid = styled.div`display: grid;grid-template-columns: 1fr;${mqs[0]} {grid-template-columns: repeat(6, 1fr);}${mqs[1]} {grid-template-columns: repeat(12, 1fr);}grid-column-gap: 40px;`;const Index = (props) => {return <Grid {...props}>{props.children}</Grid>;};export default Index;
This next step is a bit nuanced. We know that we want our site to look the same for both tablet and desktop. To do this, we only need to use a tablet media query, ${mqs[0]
. The reason for this, is that our tablet media query tells the browser that the viewport must be at least 540 pixels to implement certain styles. So any width that is greater than or equal to 540 pixels will automatically use the styles in our tablet media query.
With the applied changes, here’s what our updated Grid
styled component should look like:
// GalleryGrid/index.jsconst Grid = styled.div`display: grid;grid-template-columns: 1fr;${mqs[0]} {grid-template-columns: repeat(12, 1fr);}grid-column-gap: 40px;`;
So, let’s recap what we just did:
- We created a new Grid component that will now be used for our Gallery style setups.
- We leveraged a large amount of the existing
Grid
component code. - We reduced our media queries to just the tablet one, which uses a 12 column grid for all sizes 540px and larger. This will structure all of this content in the same way.
The Gallery Column component
Now let’s set up our Gallery Column component. We will walk through a very similar process as our GalleryGrid
component. To start, create a GalleryColumn
folder in your src
directory. Then create an index.js
file inside of your new folder. Finally, copy and paste the code from your Column/index.js
file into your GalleryColumn/index.js
file.
All together it should look like this:
// GalleryColumn/index.jsimport React from "react";import styled from "styled-components";import { mqs } from "../utils/breakpoints";import { desktopColumns, tabletColumns } from "../utils/columnSizes";const Column = styled.div`grid-column-end: -1;${mqs[0]} {${tabletColumns()};}${mqs[1]} {${desktopColumns()};grid-column-start: ${(props) => props.columnStart};}margin: 0;`;const Index = (props) => {return <Column {...props}>{props.children}</Column>;};export default Index;
Now we need to shift a few things around. We need this component to use the same column sizes for both tablet and desktop. To do this, we will need to use our Gallery Columns we set up earlier.
First, let’s remove our desktopColumns
and tabletColumns
which we will no longer be using. Our updated styled component will look like this:
// GalleryColumn/index.jsconst Column = styled.div`grid-column-end: -1;${mqs[0]} {}${mqs[1]} {grid-column-start: ${(props) => props.columnStart};}margin: 0;`;
As we discussed in our GalleryGrid
component, we only need one media query, and we can use our tablet one. One last caveat, we still want our columns to use the grid-column-start
property, so we can dynamically position our content.
In effect, our updated Column
styled component should look like this:
// GalleryColumn/index.jsconst Column = styled.div`grid-column-end: -1;${mqs[0]} {grid-column-start: ${(props) => props.columnStart};}margin: 0;`;
Now our component only uses one media query that accommodates tablet and desktop sizes and the default styles are mobile oriented. Next, we need the correct column sizes, so let’s import our galleryColumns.js
function from our utils
directory.
// GalleryColumn/index.jsimport { galleryColumns } from "../utils/galleryColumns";
Finally, we will call this function in our media query, so we can create classes for each column size. All together, the newly updated GalleryColumn
component will look like this:
// GalleryColumn/index.jsimport React from "react";import styled from "styled-components";import { mqs } from "../utils/breakpoints";import { galleryColumns } from "../utils/galleryColumns";const Column = styled.div`grid-column-end: -1;${mqs[0]} {${galleryColumns()}grid-column-start: ${(props) => props.columnStart};}margin: 0;`;const Index = (props) => {return <Column {...props}>{props.children}</Column>;};export default Index;
Making the magic happen
We now have all the ingredients we need to start making dynamic layouts with custom content positioning. To start, switch into src/App.js
, and import GalleryGrid
:
// app.jsimport GalleryGrid from "./GalleryGrid/index";
Next let’s update our app component to use GalleryGrid
:
// app.jsfunction App() {return (<div className="App"><GalleryGrid><Lookbook /></GalleryGrid></div>);}
Now, let’s import our GalleryColumn
component into our Lookbook
component:
// src/Lookbook/index.jsimport GalleryColumn from "../GalleryColumn/index";
Finally, let’s update all of our Column
components in Lookbook
to be GalleryColumn
components:
// src/Lookbook/index.jsimport React from "react";import styled from "styled-components";import Column from "../Column";import GalleryColumn from "../GalleryColumn/index";const Container = styled.div`{/* Container Styles Here */}`;const Index = () => {return (<><GalleryColumn className="m">{/* Container Content Here */}</GalleryColumn><GalleryColumn className="s" columnStart="9">{/* Container Content Here */}</GalleryColumn><GalleryColumn className="l" columnStart="3">{/* Container Content Here */}</GalleryColumn><GalleryColumn className="m">{/* Container Content Here */}</GalleryColumn><GalleryColumn className="m">{/* Container Content Here */}</GalleryColumn><GalleryColumn className="m" columnStart="3">{/* Container Content Here */}</GalleryColumn></>);};export default Index;
Alright! Let’s give this new setup a spin. Open your localhost
and you should now see this with your tablet view:
Dynamic, configurable grids
Our site now has two highly configurable and dynamic grid components. Let’s recall our original goals from the first post:
- Multiple content sizes
- Custom content positioning
- Composable content sections
- Dependable responsive states
- Maintainable and reusable code
- Quick layout iterations
Congratulations! We have accomplished all of our goals. The most exciting outcome, for me, is the ability to experiment with low time and infrastructure cost. Our content can take multiple shapes with various visual flows and all it takes is passing a few props to our Column
and GalleryColumn
components to make that happen.
Here is one of my personal experiments:
You can view the code on github as well.
Thanks for watching, I hope you have enjoyed this series. If you come up with any layouts you would like to share, please get at me on Twitter.