Search code examples
javascriptreactjsstyled-componentsreact-propsreact-component

Creating Responsive Props for Styled Components


I am trying to create responsive props for styled components as follows. To start with, we have a component (let's say a button):

<Button primary large>Click Me</Button>

This button will get a background-color of primary and a large size (as determined by a theme file).

I now want to create a responsive version of this button. This is how I would like that to work:

<Button 
  primary 
  large 
  mobile={{size: 'small', style: 'secondary'}}
  tablet={size: 'small'}} 
  widescreen={{style: 'accent'}}
>
  Click Me
</Button>

I now have my same button, but with the styles and sizes varied for different screen sizes.

Now, I have gotten this to work -- but it involves a lot of duplicate code. This is an example of what it looks like:

const Button = styled('button')(
  ({
    mobile,
    tablet,
    tabletOnly,
    desktop,
    widescreen
  }) => css`

      ${mobile &&
      css`
        @media screen and (max-width: ${theme.breakpoints.mobile.max}) {
          background-color: ${colors[mobile.style] || mobile.style};
          border: ${colors[mobile.style] || mobile.style};
          border-radius: ${radii[mobile.radius] || mobile.radius};
          color: ${mobile.style && rc(colors[mobile.style] || mobile.style)};

        }
      `}

    ${tablet &&
      css`
        @media screen and (min-width: ${theme.breakpoints.tablet.min}), print {
          background-color: ${colors[tablet.style] || tablet.style};
          border: ${colors[tablet.style] || tablet.style};
          border-radius: ${radii[tablet.radius] || tablet.radius};
          color: ${tablet.style && rc(colors[tablet.style] || tablet.style)};
        }
      `}

    ${tabletOnly &&
      css`
        @media screen and (min-width: ${theme.breakpoints.mobile.min}) and (max-width: ${theme.breakpoints.tablet.max}) {
          background-color: ${colors[tabletOnly.style] || tabletOnly.style};
          border: ${colors[tabletOnly.style] || tabletOnly.style};
          border-radius: ${radii[tabletOnly.radius] || tabletOnly.radius};
          color: ${tabletOnly.style &&
            rc(colors[tabletOnly.style] || tabletOnly.style)};
        }
      `}
`

What I am looking for is a way to simplify this code. Basically, I want to only write the CSS styles ONCE and then generate the different props and media queries based off of a query object that something like this:

const mediaQueries = {
  mobile: {
    min: '0px',
    max: '768px'
  },
  tablet: {
    print: true,
    min: '769px',
    max: '1023px'
  },
  desktop: {
    min: '1024px',
    max: '1215px'
  },
  widescreen: {
    min: '1216px',
    max: '1407px'
  },
  fullhd: {
    min: '1408px',
    max: null
  }
}

I imagine I should be able to create a function that loops through through the mediaQueries object and inserts the appropriate css for each iteration. However, I can't seem to figure out how to do this.

Any ideas on how to do this?

Also, thanks in advance for any help you can offer.


Solution

  • Maybe something like this is what you are looking for:

    import { css } from "styled-components";
    
    //mobile first approach min-width
    const screenSizes = {
      fullhd: 1408,
      widescreen: 1215,
      desktop: 1023,
      tablet: 768,
      mobile: 0
    }
    const media = Object
        .keys(screenSizes)
        .reduce((acc, label) => {
            acc[label] = (...args) => css`
                @media (min-width: ${screenSizes[label] / 16}rem) {
                    ${css(...args)}
                }
            `
            return acc
        }, {});
    

    Then you just import and use like so:

    import media from './media'
    const button = styled.button`
       ${({large , small})=> media.mobile`
          color: red;
          font-size: ${large ? '2em' : '1em'};
       `}
    `
    

    Here's some further reading including using with theming:

    Media queries in styled-components

    Utilizing Props:

    Using the same media query object from above:

    Create a helper function to format the styles object to a css string:

    const formatCss = (styleObject) => {
        return JSON.stringify(styleObject)
            .replace(/[{}"']/g,'')
            .replace(/,/g,';') 
            + ';'
    }
    

    Create another helper function to map over the styles and generate queries by mapping over its keys and using bracket notation dynamically add queries:

    const mapQueries = (myQueries) =>{
        return Object.keys(myQueries).map(key=> media[key]`
            ${formatCss(myQueries[key])}
        `)
    }
    

    In your styled-component:

    export const Button = styled.button`
        ${({myQueries}) => !myQueries ? '' : mapQueries(myQueries)}
    `
    

    Finally add a myQueries prop to your component like so (notice the use of css-formatted keys instead of javascriptFormatted keys for simplicity):

    <Button myQueries={{
        mobile:{ color:'red' },
        tablet:{ color:'blue', "background-color":'green'},
        desktop:{ height:'10rem' , width:'100%'}
    }}>Button</Button>