linaria icon indicating copy to clipboard operation
linaria copied to clipboard

Support for responsive props

Open afzalsayed96 opened this issue 3 years ago • 17 comments

Describe the feature

Is there a way to implement responsive props with linaria similar to https://github.com/emotion-js/facepaint?

Motivation

When implemening a primitive component using Box or Text, it is absolutely necessary to support responsive behaviour. Currently it is only possible to control props supplied to component externally such as

const isMobile = useMediaQuery(breakpoints.MOBILE)

<Box padding={isMobile ? 20 : 40} />

Possible implementations

It would be great if we could do something like:

<Box padding={[20, 40]} />

which could generate css media queries based on a static config.

Related Issues

Probably #465

afzalsayed96 avatar Nov 24 '20 15:11 afzalsayed96

Like in Theme-UI. I like it!

MaxSvargal-imhio avatar Dec 17 '20 08:12 MaxSvargal-imhio

Is that something linaria has on its roadmap? I'm struggling with the same use case as mentioned by @afzalsayed96, I think this pattern is quite common, and really helpful when building kit libraries. Look at mentioned Theme-UI, styled-system, rebassjs, chakra-ui, and probably much more.

mwojslaw avatar May 19 '21 07:05 mwojslaw

Idk how it can be implemented without runtime and without multiplying the size of generated styles. I mean, we can probably do something like this:

const cssWithMq = facepaint([
  '@media(min-width: 420px)',
  '@media(min-width: 920px)',
  '@media(min-width: 1120px)'
])

const myClassName = cssWithMq`
  background-color: 'hotpink';
  text-align: 'center';
  width: ${['25%', '50%', '75%', '100%']};
  & .foo: {
    color: ${['red', 'green', 'blue', 'darkorchid']};
    & img: {
      height: ${[10, 15, 20, 25]};
    }
  }
`;

but since we don't parse css, all we can do is repeat that block 4 times with media queries. And that will quadruple the size of generated css. It's not a problem for styled-components and emotion, because they generate styles on a client-side and in that case size doesn't really matter.

Anber avatar May 19 '21 09:05 Anber

Yup, probably need to wait until we could use css properties in media queries like this:

@media (min-width: --theme-breakpoint-sm}) { color: blue; }

mwojslaw avatar May 19 '21 10:05 mwojslaw

Probably, we can generate something like this:

@media(min-width: 420px) {
  --some-generated-variable-for-color-in-myClassName: 'green';
}

@media(min-width: 920px) {
  --some-generated-variable-for-color-in-myClassName: 'blue';
}

@media(min-width: 1120px) {
  --some-generated-variable-for-color-in-myClassName: 'darkorchid';
}

. myClassName {
  color: var(--some-generated-variable-for-color-in-myClassName, 'red');
}

Anber avatar May 19 '21 11:05 Anber

Yes, but the problem is occurring when those breakpoints are customizable, not static. Usually passed as theme properties. I'm assuming it can't be done at compilation time and requires some runtime to work.

mwojslaw avatar May 19 '21 12:05 mwojslaw

Yep, you are right. It will work only for constants.

Anber avatar May 19 '21 12:05 Anber

I wonder if there's a good pattern to integrate with an existing runtime-library that adds breakpoints into the render tree.

https://www.npmjs.com/package/@artsy/fresnel#why-not-conditionally-render Is nice due to it's SSR support, but it's also a bit odd that it simply ships server-rendered html for all breakpoints. Almost works against the whole point of ssr... performance.

React-responsive has some nice hooks for media queries.

devinrhode2 avatar May 29 '21 03:05 devinrhode2

@devinrhode2 I don't sure that it's what you are looking for but that plugin allows to use Linaria and styled/emotion at the same time.

Anber avatar Jun 06 '21 11:06 Anber

It's not a problem for styled-components and emotion, because they generate styles on a client-side and in that case size doesn't really matter.

Not true for SSR and SSG.

fabb avatar Jul 29 '21 05:07 fabb

Not true for SSR and SSG.

That's a good point. But do they really care about the size of styles in that case?

Anber avatar Aug 08 '21 12:08 Anber

You can kind of do this:

const breakpoints = [
  ["max-width", "40em"],
  ["min-width", "52em"],
  ["min-width", "64em"],
  ["min-width", "80em"]
]
const fontSteps = {
  1: "10px",
  2: "90px",
  3: "15px",
  4: "25px",
  6: "30px",
  7: "40px"
};
export function fontSize(values){
  const styles = {};
  breakpoints.forEach((breakpoint, idx) => {
    styles[`@media (${breakpoint[0]}: ${breakpoint[1]})`] = {
      fontSize: fontSteps[values[idx]],
    };
  });
  return styles;
}

A responsive style function like styled-system!

Then use it:

import { styled } from "@linaria/react";

export const H1 = styled.h1`
 ... other styles ...
  ${fontSize([4, 6, 7])}
`;

I've struggled to find a way to pass in the [4, 6, 7] via props but can't get it to work. That seems like the only missing piece. Also, as soon as I start abstracting away that line styles[`@media (${breakpoint[0]}: ${breakpoint[1]})`] I get a Linaria error saying the static compiler can't infer the styles (or something along those lines). But I'm happy enough with zero runtime if I have to duplicate code. This is pretty much the best of both worlds.

engelmav avatar Sep 28 '21 21:09 engelmav

I've struggled to find a way to pass in the [4, 6, 7] via props but can't get it to work.

I'm afraid, that this is impossible because props interpolation is implemented through css variables, that can be used only as values of css properties.

Anber avatar Oct 07 '21 11:10 Anber

@Anber can not this be achieved at the component level by using matchMedia under the hood

joseDaKing avatar Nov 23 '21 18:11 joseDaKing

@joseDaKing will it make Linaria a non-zero-runtime library? :)

Anber avatar Nov 29 '21 12:11 Anber

Idk how it can be implemented without runtime and without multiplying the size of generated styles. I mean, we can probably do something like this:

const cssWithMq = facepaint([
  '@media(min-width: 420px)',
  '@media(min-width: 920px)',
  '@media(min-width: 1120px)'
])

const myClassName = cssWithMq`
  background-color: 'hotpink';
  text-align: 'center';
  width: ${['25%', '50%', '75%', '100%']};
  & .foo: {
    color: ${['red', 'green', 'blue', 'darkorchid']};
    & img: {
      height: ${[10, 15, 20, 25]};
    }
  }
`;

but since we don't parse css, all we can do is repeat that block 4 times with media queries. And that will quadruple the size of generated css.

Would it be an option to start parsing the css string and only repeat the css attribute for the according responsive values, or would compile times explode? With object interpolations it would even be simpler, no css parsing necessary but I guess we need a solution that works for template literals too.

maybe an alternative would be something like this? Not as nice to read though:

const medias = [
  '@media(min-width: 420px)',
  '@media(min-width: 920px)',
  '@media(min-width: 1120px)'
]

const responsive = (attribute, responsiveValues) => responsiveValues.map((value, index) => index === 0 ? `${attribute}: ${value}` : `${medias[index-1]} { ${attribute}: ${value} }`).join(';'))

const myClassName = css`
  background-color: 'hotpink';
  text-align: 'center';
  ${responsive('width', ['25%', '50%', '75%', '100%']};
`;

another option I could think of would be a PostCSS plugin that turns array values into media queries, but i think that transpilation would be better done inside linaria for encapsulation reasons.

fabb avatar Feb 03 '22 07:02 fabb

I don't think it's a silver bullet, but it may be worth seeing how the 4x larger css compresses under gzip, and then serving that with an http2/spdy server.. and of course, browser parse time will also need to be measured

devinrhode2 avatar Feb 07 '22 05:02 devinrhode2