0
votes

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.

2

2 Answers

3
votes

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>
0
votes

For iterating through all your media queries, you can create a function similar to:

import { css } from "styled-components";

const sizes = {
  desktop: 992,
  tablet: 768,
  phone: 576
};

// Iterate through the sizes and create a media template
const media = Object.keys(sizes).map(screenLabel => {
  return {
    query: (...args) => css`
      @media (max-width: ${sizes[screenLabel] / 16}em) {
        ${css(...args)}
      }
    `,
    screenLabel
  };
});

export default media;

For usage in the component:

import media from "./media";

// The labels for this has to be same as the ones in sizes object
const colors = {
  phone: "red",
  tablet: "yellow",
  desktop: "green"
};

const Heading = styled.h2`
  color: blue;

  ${media.map(
    ({ query, screenLabel }) => query`
    color: ${colors[screenLabel]};
  `
  )}
`;