Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Typography - Feature request: font size responsiveness #11452

Closed
PolGuixe opened this issue May 17, 2018 · 19 comments · Fixed by #14573
Closed

Typography - Feature request: font size responsiveness #11452

PolGuixe opened this issue May 17, 2018 · 19 comments · Fixed by #14573
Labels
component: Typography The React component. docs Improvements or additions to the documentation priority: important This change can make a difference

Comments

@PolGuixe
Copy link
Contributor

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
Copy link
Member

oliviertassinari commented May 17, 2018

@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
Copy link
Contributor Author

@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 oliviertassinari added component: Typography The React component. waiting for 👍 Waiting for upvotes and removed waiting for user information labels May 20, 2018
@oliviertassinari
Copy link
Member

oliviertassinari commented May 20, 2018

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
Copy link
Member

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
Copy link
Contributor Author

Ok, fair enough 😉

@oliviertassinari oliviertassinari added docs Improvements or additions to the documentation and removed waiting for 👍 Waiting for upvotes labels Jan 15, 2019
@oliviertassinari
Copy link
Member

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

@PolGuixe
Copy link
Contributor Author

@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
Copy link
Member

oliviertassinari commented Jan 21, 2019

@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.

@oliviertassinari oliviertassinari added the priority: important This change can make a difference label Jan 21, 2019
@PolGuixe
Copy link
Contributor Author

@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
Copy link
Member

oliviertassinari commented Jan 22, 2019

@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
Copy link
Contributor Author

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
Copy link

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
Copy link
Contributor Author

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

@oliviertassinari
Copy link
Member

oliviertassinari commented Jan 24, 2019

@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
Copy link
Contributor Author

PolGuixe commented Jan 31, 2019

@oliviertassinari has this helper been implemented?

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

@oliviertassinari
Copy link
Member

oliviertassinari commented Jan 31, 2019

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

@PolGuixe
Copy link
Contributor Author

PolGuixe commented Jan 31, 2019

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
Copy link
Member

oliviertassinari commented Feb 13, 2019

@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
Copy link
Contributor

n-batalha commented Feb 15, 2019

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].

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component: Typography The React component. docs Improvements or additions to the documentation priority: important This change can make a difference
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants