mui / material-ui

Material UI: Comprehensive React component library that implements Google's Material Design. Free forever.
https://mui.com/material-ui/
MIT License
93.42k stars 32.15k forks source link

Typography - Feature request: font size responsiveness #11452

Closed PolGuixe closed 5 years ago

PolGuixe commented 6 years ago

Currently, the Typography variants are quite rigid. It will be really useful if they could become responsive e.g. different font-sizes according to the screen size.

What do you think? Would you consider a PR in that direction? 🤔

oliviertassinari commented 6 years ago

@PolGuixe What's preventing from doing it now? The theme.typograpgy.xxx objects are interpreted as CSS. So you can always use a breakpoint.

PolGuixe commented 6 years ago

@oliviertassinari you are right.

I just was wondering if you think it would be useful to have it integrate it in Material-UI.

What I am doing right now is:


const theme = createMuiTheme({
  palette,
  typography,
});

const coef = 0.1;
const modifyRem = (value, coef) => {
  return `${parseFloat(value) * (1 + coef)}rem`;
};

each(theme.typography, (variant, variantName) => {
  if (typeof variant !== 'object') {
    return variant;
  }
  theme.typography[variantName] = {
    ...variant,
    fontSize: modifyRem(variant.fontSize, -coef * 5),
    [theme.breakpoints.up('sm')]: {
      fontSize: modifyRem(variant.fontSize, -coef * 2.5),
    },
    [theme.breakpoints.up('md')]: {
      fontSize: modifyRem(variant.fontSize, -coef * 1),
    },
    [theme.breakpoints.up('lg')]: {
      fontSize: modifyRem(variant.fontSize, 0),
    },
    [theme.breakpoints.up('xl')]: {
      fontSize: modifyRem(variant.fontSize, coef),
    },
  };
});

export default theme;

Note 1: the numbers are a bit random as I couldn't find any guidelines from Material Note 2: the logic could be improved to be more granular e.g. different coefficients for each breakpoint. Note 3: maybe it could be integrated within the createTypography function. The default coefficient could be 0 and if any user needs it they can set up their own.

For us it works, because having a display4on an xs screen is normally is not very useful in most of the cases.

oliviertassinari commented 6 years ago

it would be useful to have it integrate it in Material-UI

@PolGuixe This sounds too opinionated. You couldn't find this logic on Bootstrap, on Ant Design, on Semantic-UI, on Blueprint, on Fabric. Thanks for sharing the workaround! Maybe we could document it at some point.

oliviertassinari commented 6 years ago

I have added the waiting for users upvotes tag. I'm closing the issue as I'm not sure people are looking for such abstraction. So please upvote this issue if you are. We will prioritize our effort based on the number of upvotes.

PolGuixe commented 6 years ago

Ok, fair enough 😉

oliviertassinari commented 5 years ago

I think that we should add an example in the documentation with the best strategy.

PolGuixe commented 5 years ago

@oliviertassinari Cool I can do it ;)

Do you still think that the approach above is the best? Does anyone have other ideas?

We are using the approach above in production and it works. Every project has a slightly different configuration for their theme, font selection and look and feel.

Is there a default config we would like to have? https://material.io/design/typography/understanding-typography.html# Doesn't have a particular spec for font-size vs screen size.

oliviertassinari commented 5 years ago

@PolGuixe I have found two interesting approaches using a different tradeoff:

What do you think of them? I think that we should move & isolate all our custom CSS-in-JS helpers into a @material-ui/css-helpers package.

PolGuixe commented 5 years ago

@oliviertassinari If we are happy to use calc() in CSS I'll go for the first approach of 4 params. I've found that being able to set-up maximum and minimum font sizes are better than playing with a sizing factor. Although it might, be difficult to find a coherent logic to apply this transformation to each of the variants. I can use these 4 parameters set to the body1 variant and then calculate a linear factor to be applied with a weight to the other variants. It may sound a bit complicated to understand for the user.

I agree with the approach to isolate the helpers.

I see the function working like:

import makeTypographyResponsive from '@material-ui/css-helpers/makeTypographyResponsive'

import theme from './theme';
// or define theme here...

const newTheme = makeThemeResponsive(theme, {minSize: {size:'12px', down: 'xs'}, maxSize:{size: '24px', up: 'md' });  // Maybe we should use ‘rem’ units.

// export etc

This function will be used to implement most of the Typography responsiveness. Then the user will be always able to do manual tweaks:

 theme.typography[variantName] = {
    fontSize: //…
}
oliviertassinari commented 5 years ago

@PolGuixe I was thinking of the following:

import { fluidRange } from '@material-ui/css-helpers';
import { createMuiTheme } from '@material-ui/core';

const theme = createMuiTheme();

function remToPx(value) {
  return Math.round(parseFloat(value) * 16);
}

['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'subtitle1', 'subtitle2', 'body1', 'body2', 'buttonNext', 'captionNext', 'overline'].forEach(variant => {
  const style = theme.typography[variant];
  const pixelFontSize = remToPx(style.fontSize);

  if (pixelFontSize <= 20) {
    return;
  }

  theme.typography[variant] = {
    ...style,
    ...fluidRange({
      cssProperty: 'fontSize',
      min: pixelFontSize * 0.5,
      max: pixelFontSize,
      lowerRange: 300,
      higherRange: theme.breakpoints.values.md, // 960
    }),
  };
})

It focuses on providing a lower level helper, without being opinionated on how the responsive style should behave. What do you think?

PolGuixe commented 5 years ago

I think is a good approach and it can work. I'll have to test it to check the outputs. I'll probably rewrite the code above as the following to avoid specifying the variants array:

import { fluidRange } from '@material-ui/css-helpers';
import { createMuiTheme } from '@material-ui/core';

const theme = createMuiTheme();

function remToPx(value) {
  return Math.round(parseFloat(value) * 16);
}

Object.entries(theme.typography).forEach((variantName, variant) => {
  if (typeof variant !== 'object') {
    return;
  }
  const pixelFontSize = remToPx(variant.fontSize);

  if (pixelFontSize <= 20) {
    return;
  }

  theme.typography[variantName] = {
    ...variant,
    ...fluidRange({
      cssProperty: 'fontSize',
      min: pixelFontSize * 0.5,
      max: pixelFontSize,
      lowerRange: 300,
      higherRange: theme.breakpoints.values.md, // 960
    }),
  };
})

To encapsulate the logic as a function we can do the following:

// ../css-helpers/makeTypographyResponsive.js
import {fluidRange} from './fluidRange'

function remToPx(value) {
  return Math.round(parseFloat(value) * 16);
}

const makeTypographyResponsive = (theme, options = {minFontSize:20, resizeFactor: 0.5, lowerRange: 300, higherRange: 960}) => {

Object.entries(theme.typography).forEach((variantName, variant) => {
  if (typeof variant !== 'object') {
    return;
  }
  const pixelFontSize = remToPx(variant.fontSize);

  if (pixelFontSize <= options.minFontSize) {
    return;
  }

  theme.typography[variantName] = {
    ...variant,
    ...fluidRange({
      cssProperty: 'fontSize',
      min: pixelFontSize * options.resizeFactor,
      max: pixelFontSize,
      lowerRange: options.lowerRange,
      higherRange: options.higherRange,
    }),
  };
})

}

And use it as:

import { makeTypographyResponsive } from '@material-ui/css-helpers';
import { createMuiTheme } from '@material-ui/core';

const theme = makeTypographyResponsive(createMuiTheme());

Would you create this function or just put the logic as an example?

jalcalav commented 5 years ago

I suggest the following solution:

import createBreakpoints from "@material-ui/core/styles/createBreakpoints";
const breakpoints = createBreakpoints({});
const styles = {
  "@global": {
    html: {
      [breakpoints.up("xs")]: {
        fontSize: "8px"
      },
      [breakpoints.up("sm")]: {
        fontSize: "12px"
      },
      [breakpoints.up("md")]: {
        fontSize: "14px"
      },
      [breakpoints.up("lg")]: {
        fontSize: "16px"
      }
    }
  }
};

export default withStyles(styles)(MyApp);

It's working for me. Uses withStyles in the main component and applies everywhere without any changes.

Hope it helps.

PolGuixe commented 5 years ago

@jalcalav I believe we are looking for a solution that integrates with the Typography component and works with the different variants.

oliviertassinari commented 5 years ago

@jalcalav I wouldn't recommend changing the <html> font size as it's overriding the user's font size accessibility preference. Please, don't do it.

PolGuixe commented 5 years ago

@oliviertassinari has this helper been implemented?

import { fluidRange } from '@material-ui/css-helpers';
oliviertassinari commented 5 years ago

@PolGuixe No, but I think that we should! I love the potential.

PolGuixe commented 5 years ago
fluidRange({cssProperty, min, max, range=[300, 960]}){
  // calculate liner regression
  const factor = (max-min)/(range[range.lenght-1]-range[0]);
  const constant = min - factor*range[0];

  //add media queries for each range value
  range.each(value =>{
    style[`@media (max-width:${value}px)`]={
       [cssProperty]: `${factor*value + constant}px`
    }
  })

 return style;
}

should it work along the lines of this logic?

Disclaimer: not tested, just a draft 😉

oliviertassinari commented 5 years ago

@PolGuixe I have made a proof of concept with a simple version (we could support different units).

function fluidRange({ cssProperty, min, max, lowerRange = 400, higherRange = 960 }) {
  const factor = Math.round(((max - min) / (higherRange - lowerRange)) * 10000) / 10000;

  return {
    [cssProperty]: `${min}px`,
    [`@media (min-width:${lowerRange}px)`]: {
      [cssProperty]: `calc(${min}px  (100vw - ${lowerRange}px) * ${factor})`,
    },
    [`@media (min-width:${higherRange}px)`]: {
      [cssProperty]: `${max}px`,
    },
  };
}

function remToPx(value) {
  return Math.round(parseFloat(value) * 16);
}

function responsiveTypography(theme, { minFontSize, scale, ...other }) {
  const output = theme;
  output.typography = { ...theme.typography };

  [
    'h1',
    'h2',
    'h3',
    'h4',
    'h5',
    'h6',
    'subtitle1',
    'subtitle2',
    'body1',
    'body2',
    'buttonNext',
    'captionNext',
    'overline',
  ].forEach(variant => {
    const style = output.typography[variant];
    const pixelFontSize = remToPx(style.fontSize);

    if (pixelFontSize <= minFontSize) {
      return;
    }

    output.typography[variant] = {
      ...style,
      ...fluidRange({
        cssProperty: 'fontSize',
        min: Math.max(minFontSize, Math.round(pixelFontSize * scale)),
        max: pixelFontSize,
        ...other,
      }),
    };
  });

  return output;
}

You would use it like this:

 function getTheme(uiTheme) {
-  const theme = createMuiTheme({
+  let theme = createMuiTheme({
     direction: uiTheme.direction,
     nprogress: { color: uiTheme.paletteType === 'light' ? '#000' : '#fff' },
     palette: { ...uiTheme.paletteColors, type: uiTheme.paletteType },
     typography: { useNextVariants: true },
   });

+  theme = responsiveTypography(theme, {
+    minFontSize: 14,
+    scale: 0.7,
+  });
+
   // Expose the theme as a global variable so people can play with it.
   if (process.browser) {
     window.theme = theme;

It's definitely something we could wrap in a @material-ui/css-kitpackage, if someone is interested in helping out 👋 :).

n-batalha commented 5 years ago

I wanted to help but there is something I don't understand re: vertical rhythm.

@oliviertassinari you quoted this nice article which makes rhythm seem easy (as does the Gutenberg package by the same author), but the approach there relies on all line heights being a multiple of (1 or 0.5)x(base line height). I looked at some rhythm preserving css generators and they all seem to do it this way (*). But material-ui gives line heights 96, 60, 49.916, 39.78, 31.9167 etc (px) and I'm not finding any pattern. Edit: unless 4px (0.25 of base line height) is the "rhythm" - and somehow ignoring the ~50px case of h3 - as per minimum requirement of Material Design (**).

Presumably if fonts are made responsive this API would allow users to easily preserve vertical rhythm? Or at least the 4px MD requirement?

(*) 0, 1, 2, 3

(**) 0, 1 [presumably 'dp' which equals px on web].