DEV Community

Cover image for Creating a Reusable Grid System in React
Jarod Peachey
Jarod Peachey

Posted on

Creating a Reusable Grid System in React

The grid system is arguably the most valuable layout tool for building websites. Without it, responsive layouts would be, well, NOT responsive.

I use React a lot, so I decided to create a grid system that I could reuse in my React apps. It started as a personal tool, but as I got more use out of it, I decided to release it for other devs to use.

So I did. It's called React Tiny Grid, and it's a 12-column grid system that's pretty handy. You can find it here.

But today, we're going to rebuild it step-by-step, so you can follow along and see how it's built.

Setup

We'll be using styled-components to style our grid system. Let's install that.

$ npm install --save styled-components

Now that we've got our dependencies installed, we'll create our two files: one for the Row component, and one for the Column component.

$ touch Row.js Column.js

Basic Grid Functionality

To start, we'll create a basic flex wrapper that makes all the column items the same width, and wraps them.

Creating the Row Component

Inside of our Row.js file, we'll outline the basic row component.

import  React  from  'react';
import  styled, { css } from  'styled-components';
import { Column } from  './Column';

export const Row = ({children}) => {
  return (
    <Wrapper>
      {React.Children.toArray(children).map((item) => {
        return (
          item && (
            <Column>
              {item.props.children}
            </Column>
          )
        );
      })}
    </Wrapper>
  );
};

const  Wrapper = styled.div`
  @media (min-width: 769px) {
    display: flex;
    justify-content: flex-start;
    flex-wrap: wrap;
    margin: 0 -8px 0 -8px
  }
`;

Let's break this down.

For the basic functionality, we're mapping through the children of this component, and making those each a Column (we'll style those later).

{React.Children.toArray(children).map((item) => {
  return (
    item && (
      <Column>
        {item.props.children}
      </Column>
    )
  );
})}

To add the grid functionality, we simply make the <Wrapper> a flex element.

const  Wrapper = styled.div`
  @media (min-width: 769px) {
    display: flex;
    justify-content: flex-start;
    flex-wrap: wrap;
    margin: 0 -8px 0 -8px;
  }
`;

We 'activate' the grid system once the screen size is wider than 769px. Then, we set the display to flex.

We also add the negative margin to account for the spacing of the columns (styled later).

margin: 0 -8px 0 -8px;

Creating the Column Component

Now that we have our Row component, we need to style the Column component.

Inside of our Column.js file, we'll create the basic column markup and styles.

import  React  from  'react';
import  styled, { css } from  'styled-components';

export const Column = ({children}) => {
  return (
    <Wrapper>{children}</Wrapper>
  );
};

const Wrapper = styled.div`
  flex: 1 1 0;
  width: 100%;
  padding: 8px;
`;

All we have to do for now is give the Column the ability to resize equally with it's siblings. This is accomplished using the flex property.

flex: 1 1 0;

We also added 8px of padding to each column. If you recall, that's the amount of negative margin we added to the Row component. This is to make sure the edges of the columns meet the edges of their parent container.

Supporting Custom Breakpoints

So far, we've got an automatic grid system! The columns are all resized, and are full-width on mobile.

But a REAL grid system supports custom breakpoints. So let's do that now.

Inside of our Row.js file, we'll accept a breakpoints prop, with a default value of 769.

export const Row = ({children, breakpoints = [769]}) => {
  ...
};

Now, we can use this breakpoints array to decide when to active the grid. To do this, we pass the first item in the breakpoints array to the <Wrapper> component.

export const Row = ({children}) => {
  return (
    <Wrapper breakpoint={breakpoints[0]}>
      ...
    </Wrapper>
  );
};

Then, we replace the 769px media query with a template literal, which are supported by styled-components. This allows us to use our breakpoint value.

const  Wrapper = styled.div`
  @media (min-width: ${props => props.breakpoint}px) {
    ...
  }
`;

Now, we can pass in a custom breakpoint to our Row component.

<Row breakpoints={[960]} />

But you know what would be cool?

Custom column widths. For each breakpoint 🤯

Let's do that now!

Custom Widths

Back inside of our Column.js file, we need to accept two new props: first, a breakpoints array, which will be passed down from the parent Row component. Second, a widths array, which will contain an array of numbers defining how many columns to take up.

export const Column = ({children, breapoints, widths = ['auto']}) => {
  ...
};

Note: we used a default value of auto for the widths: this will allow the column to take up whatever space is available, in case we forget to pass in a widths prop.

Now, we're setting up the grid system to support up to three custom breakpoints and widths. However, we need to make sure that we have a default value for each of these three, in case we forget to pass in a value.

At the top of our Column component, we'll add these variables.

const  breakpointOne = breakpoints[0];
const  breakpointTwo = breakpoints.length >= 1 ? breakpoints[1] : null;
const  breakpointThree = breakpoints.length >= 2 ? breakpoints[2] : null;

const  widthOne = widths[0];
const  widthTwo = widths.length >= 1 ? widths[1] : null;
const  widthThree = widths.length >= 2 ? widths[2] : null;

Basically, what we're doing is checking if there are 3 width values. If not, we set the third value to the previous width item. That way, our grid won't break!

Now, we need to pass in these values as props to the column <Wrapper> component.

export const Column = ({children, breakpoints, widths = ['auto']}) => {
  return (
    <Wrapper
      breakpointOne={breakpointOne}
      breakpointTwo={breakpointTwo}
      breakpointThree={breakpointThree}
      widthOne={widthOne}
      widthTwo={widthTwo}
      widthThree={widthThree}
    >
      {children}
    </Wrapper>
  );
};

This will allow us to change the width of the column based on specific breakpoints.

Inside our Wrapper styled-component, let's add media queries.

const Wrapper = styled.div`
  flex: 1 1 0;
  width: 100%;
  padding: 8px;

  // ACTIVE BETWEEN BREAKPOINT ONE AND TWO (OR 9999PX)
  @media(min-width: ${props => props.breakpointOne}px) and
  (max-width: ${props => props.breakpointTwo | 9999}px) {
    width: ${props => props.widthOne !== 'auto'
      ? `${(props.widthOne / 12) * 100}%`
      : null};
    flex: ${(props) => (props.widthOne !== 'auto' ? 'none !important' : null)};
  }

  // ACTIVE BETWEEN BREAKPOINT TWO AND THREE (OR 9999PX)
  @media(min-width: ${props => props.breakpointTwo}px) and
  (max-width: ${props => props.breakpointThree | 9999}px) {
    width: ${props => props.widthTwo !== 'auto'
      ? `${(props.widthTwo / 12) * 100}%`
      : null};
    flex: ${(props) => (props.widthTwo !== 'auto' ? 'none !important' : null)};
  }

  // ACTIVE BETWEEN BREAKPOINT THREE AND UP
  @media(min-width: ${props => props.breakpointThree}px) {
    width: ${props => props.widthThree !== 'auto'
      ? `${(props.widthThree / 12) * 100}%`
      : null};
    flex: ${(props) => (props.widthThree !== 'auto' ? 'none !important' : null)};
  }
`;

Ok. That's a lot to look at.

The first thing we make sure to do is add a max-width to the media query. This is to make sure the flex property does NOT get reset if the width value is 'auto'.

The main thing we have to take note of is the function used to calculate the width of the column. Since we use a 12-column grid, we get this value by taking the width (a value from 1-12) and dividing it by 12. We multiply THAT number by 100 to get the percentage.

width: ${props => props.widthThree !== 'auto' ? `${(props.widthThree / 12) * 100}%` : null};

We also add a ternary operator to make sure the width is still 100% if the column width is auto by setting the width value to null.

Now, the final thing we need to do is pass the breakpoints from the Row component to the Column component.

Inside of our Row.js file, we'll update the return statement.

return (
  {React.Children.toArray(children).map((item) => {
    return (
      item && (
        <Column
          breakpoints={breakpoints}
          {...item.props}
        >
          {item.props.children}
        </Column>
      )
    );
  })}
)

And viola! Now, we're able to use custom breakpoints and widths for our grid system.

<Row breakpoints={[576]}>   
 <Column widths={[4]} />  
 <Column widths={[8]} />  
 <Column widths={[3]} />  
 <Column widths={[9]} />  
 <Column widths={[7]} />  
 <Column widths={[5]} />  
</Row>

Conclusion

So now, we have a fully-functioning React grid system. If you want even more functionality, like custom spacing, offsets, and more, check out React Tiny Grid.

You can find the full code for this grid system on Github.

If you liked this tutorial and found React Tiny Grid useful, I'd appreciate it if you could buy me a coffee!

If you've got any questions or improvements for the grid system, you can comment that down below.

Oldest comments (4)

Collapse
 
singhacz profile image
singha-cz

That's a cool lib, man. Especially if you don't want to or for some reason can't use a framework like Bootstrap, Material UI or Ant Design. They usually have much more than just grid available for a developer to utilize. I find most of the time that is the case.

Collapse
 
jarodpeachey profile image
Jarod Peachey

Thanks! Yeah, at first I was hesitant because there are tons of UI kits with native grid systems. But I couldn't find one that was auto-responsive without any props.

And you're right, this is a lot simpler. It's only supposed to be a grid, nothing more, nothing less.

Collapse
 
meridjanassim profile image
Meridja Nassim

Great library, suitable for most use-cases, I will consider it in my future projects

Collapse
 
jarodpeachey profile image
Jarod Peachey

Thank you! I'm glad you like it!