Creating modular layouts in React, part 2

Create Dynamic Layouts in React using Styled Components and CSS Grid
This post is a continuation of Modular Layouts in React Part 1. In the first part, we discussed dynamic grids and how to achieve them. Now, we will pick up where we left off and discuss how to achieve custom content positioning.

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/start
yarn install
yarn 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:

homepage start screen

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.

Loading Video ...

So, to accomplish custom content positioning, we will need to update our Column component in two ways:

  1. to use grid-column-start in our desktop view.
  2. to inherit a columnStart prop from each Column 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.js
const 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.js
import 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 a className property to determine the width of the section.
  • Some Column components also include a columnStart 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 here
import 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:

Loading Video ...

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.js
const 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.js
export 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.js
import 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.js
const 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.js
import 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.js
const 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.js
const 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.js
import { 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.js
import 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.js
import GalleryGrid from "./GalleryGrid/index";

Next let’s update our app component to use GalleryGrid:

// app.js
function App() {
return (
<div className="App">
<GalleryGrid>
<Lookbook />
</GalleryGrid>
</div>
);
}

Now, let’s import our GalleryColumn component into our Lookbook component:

// src/Lookbook/index.js
import GalleryColumn from "../GalleryColumn/index";

Finally, let’s update all of our Column components in Lookbook to be GalleryColumn components:

// src/Lookbook/index.js
import 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:

Loading Video ...

Dynamic, configurable grids

Our site now has two highly configurable and dynamic grid components. Let’s recall our original goals from the first post:

  1. Multiple content sizes
  2. Custom content positioning
  3. Composable content sections
  4. Dependable responsive states
  5. Maintainable and reusable code
  6. 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:

Loading Video ...

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.