diff --git a/.circleci/config.yml b/.circleci/config.yml
index a792a9e266cadf..a78d4ecfec98e0 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -31,7 +31,6 @@ commands:
command: |
sudo apt-get update
sudo apt-get install -y --force-yes gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
-
jobs:
checkout:
@@ -156,7 +155,7 @@ jobs:
name: Can we generate the documentation?
command: yarn docs:api
- run:
- name: '`yarn docs:api` changes commited?'
+ name: '`yarn docs:api` changes committed?'
command: git diff --exit-code
- run:
name: Can we generate the @material-ui/core build?
diff --git a/docs/src/pages/customization/themes/ResponsiveFontSizes.js b/docs/src/pages/customization/themes/ResponsiveFontSizes.js
new file mode 100644
index 00000000000000..ca17c0db0756e4
--- /dev/null
+++ b/docs/src/pages/customization/themes/ResponsiveFontSizes.js
@@ -0,0 +1,15 @@
+import React from 'react';
+import { createMuiTheme, responsiveFontSizes } from '@material-ui/core/styles';
+import { ThemeProvider } from '@material-ui/styles';
+import Typography from '@material-ui/core/Typography';
+
+let theme = createMuiTheme();
+theme = responsiveFontSizes(theme);
+
+export default function ResponsiveFontSizes() {
+ return (
+
+ Responsive h3
+
+ );
+}
diff --git a/docs/src/pages/customization/themes/themes.md b/docs/src/pages/customization/themes/themes.md
index 8432891c4a0381..4093770c1a4171 100644
--- a/docs/src/pages/customization/themes/themes.md
+++ b/docs/src/pages/customization/themes/themes.md
@@ -345,6 +345,35 @@ html {
{{"demo": "pages/customization/themes/FontSizeTheme.js"}}
+### Responsive font sizes
+
+The typography variants properties map directly to the generated CSS.
+You can use [media queries](/layout/breakpoints/#api) inside them:
+
+```js
+const theme = createMuiTheme();
+
+theme.typography.h1 = {
+ fontSize: '3rem',
+ '@media (min-width:600px)': {
+ fontSize: '4.5rem',
+ },
+ [theme.breakpoints.up('md')]: {
+ fontSize: '6rem',
+ },
+};
+```
+
+To automate this setup, you can use the [`responsiveFontSizes()`](#responsivefontsizes-theme-options-theme) helper to make Typography font sizes in the theme responsive.
+
+You can see this in action in the example below. adjust your browser's window size, and notice how the font size changes as the width crosses the different [breakpoints](/layout/breakpoints/):
+
+{{"demo": "pages/customization/themes/ResponsiveFontSizes.js"}}
+
+### Fluid font sizes
+
+To be done: [#15251](https://github.com/mui-org/material-ui/issues/15251).
+
## Spacing
We encourage you to use the `theme.spacing()` helper to create consistent spacing between the elements of your UI.
@@ -512,3 +541,33 @@ const theme = createMuiTheme({
},
});
```
+
+### `responsiveFontSizes(theme, options) => theme`
+
+Generate responsive typography settings based on the options received.
+
+#### Arguments
+
+1. `theme` (*Object*): The theme object to enhance.
+2. `options` (*Object* [optional]):
+
+ - `breakpoints` (*Array* [optional]): Default to `['sm', 'md', 'lg']`. Array of [breakpoints](/layout/breakpoints/) (identifiers).
+ - `disableAlign` (*Boolean* [optional]): Default to `false`. Whether font sizes change slightly so line
+ heights are preserved and align to Material Design's 4px line height grid.
+ This requires a unitless line height in the theme's styles.
+ - `factor` (*Number* [optional]): Default to `2`. This value determines the strength of font size resizing. The higher the value, the less difference there is between font sizes on small screens.
+ The lower the value, the bigger font sizes for small screens. The value must me greater than 1.
+ - `variants` (*Array* [optional]): Default to all. The typography variants to handle.
+
+#### Returns
+
+`theme` (*Object*): The new theme with a responsive typography.
+
+#### Examples
+
+```js
+import { createMuiTheme, responsiveFontSizes } from '@material-ui/core/styles';
+
+let theme = createMuiTheme();
+theme = responsiveFontSizes(theme);
+```
diff --git a/packages/material-ui/package.json b/packages/material-ui/package.json
index 3b1baf6ca6cc24..0a54696b0fccf6 100644
--- a/packages/material-ui/package.json
+++ b/packages/material-ui/package.json
@@ -43,6 +43,7 @@
"@types/react-transition-group": "^2.0.16",
"clsx": "^1.0.2",
"csstype": "^2.5.2",
+ "convert-css-length": "^1.0.2",
"debounce": "^1.1.0",
"deepmerge": "^3.0.0",
"hoist-non-react-statics": "^3.2.1",
diff --git a/packages/material-ui/src/styles/createTypography.js b/packages/material-ui/src/styles/createTypography.js
index 02e3b2f78050d8..eac9d2474a95d9 100644
--- a/packages/material-ui/src/styles/createTypography.js
+++ b/packages/material-ui/src/styles/createTypography.js
@@ -65,6 +65,7 @@ export default function createTypography(palette, typography) {
return deepmerge(
{
+ htmlFontSize,
pxToRem,
round,
fontFamily,
diff --git a/packages/material-ui/src/styles/cssUtils.js b/packages/material-ui/src/styles/cssUtils.js
new file mode 100644
index 00000000000000..a1bfda8971dec0
--- /dev/null
+++ b/packages/material-ui/src/styles/cssUtils.js
@@ -0,0 +1,73 @@
+export function alignProperty({ size, grid }) {
+ const sizeBelow = size - (size % grid);
+ const sizeAbove = sizeBelow + grid;
+
+ return size - sizeBelow < sizeAbove - size ? sizeBelow : sizeAbove;
+}
+
+// fontGrid finds a minimal grid (in rem) for the fontSize values so that the
+// lineHeight falls under a x pixels grid, 4px in the case of Material Design,
+// without changing the relative line height
+export function fontGrid({ lineHeight, pixels, htmlFontSize }) {
+ return pixels / (lineHeight * htmlFontSize);
+}
+
+/**
+ * generate a responsive version of a given CSS property
+ * @example
+ * responsiveProperty({
+ * cssProperty: 'fontSize',
+ * min: 15,
+ * max: 20,
+ * unit: 'px',
+ * breakpoints: [300, 600],
+ * })
+ *
+ * // this returns
+ *
+ * {
+ * fontSize: '15px',
+ * '@media (min-width:300px)': {
+ * fontSize: '17.5px',
+ * },
+ * '@media (min-width:600px)': {
+ * fontSize: '20px',
+ * },
+ * }
+ *
+ * @param {Object} params
+ * @param {string} params.cssProperty - The CSS property to be made responsive
+ * @param {number} params.min - The smallest value of the CSS property
+ * @param {number} params.max - The largest value of the CSS property
+ * @param {number} [params.unit] - The unit to be used for the CSS property
+ * @param {Array.number} [params.breakpoints] - An array of breakpoints
+ * @param {number} [params.alignStep] - Round scaled value to fall under this grid
+ * @returns {Object} responsive styles for {params.cssProperty}
+ */
+export function responsiveProperty({
+ cssProperty,
+ min,
+ max,
+ unit = 'rem',
+ breakpoints = [600, 960, 1280],
+ transform = null,
+}) {
+ const output = {
+ [cssProperty]: `${min}${unit}`,
+ };
+
+ const factor = (max - min) / breakpoints[breakpoints.length - 1];
+ breakpoints.forEach(breakpoint => {
+ let value = min + factor * breakpoint;
+
+ if (transform !== null) {
+ value = transform(value);
+ }
+
+ output[`@media (min-width:${breakpoint}px)`] = {
+ [cssProperty]: `${Math.round(value * 10000) / 10000}${unit}`,
+ };
+ });
+
+ return output;
+}
diff --git a/packages/material-ui/src/styles/cssUtils.test.js b/packages/material-ui/src/styles/cssUtils.test.js
new file mode 100644
index 00000000000000..c4ec0692b3396f
--- /dev/null
+++ b/packages/material-ui/src/styles/cssUtils.test.js
@@ -0,0 +1,99 @@
+import { assert } from 'chai';
+import { alignProperty, fontGrid, responsiveProperty } from './cssUtils';
+
+describe('cssUtils', () => {
+ describe('alignProperty', () => {
+ const tests = [
+ { args: { size: 8, grid: 4 }, expected: 8 },
+ { args: { size: 8, grid: 1 }, expected: 8 },
+ { args: { size: 8, grid: 9 }, expected: 9 },
+ { args: { size: 8, grid: 7 }, expected: 7 },
+ { args: { size: 8, grid: 17 }, expected: 0 },
+ ];
+
+ tests.forEach(test => {
+ const {
+ args: { size, grid },
+ expected,
+ } = test;
+
+ it(`aligns ${size} on grid ${grid} to ${expected}`, () => {
+ const sizeAligned = alignProperty({ size, grid });
+ assert.strictEqual(sizeAligned, expected);
+ });
+ });
+ });
+
+ describe('fontGrid', () => {
+ const tests = [
+ { lineHeight: 1.3, pixels: 4, htmlFontSize: 16 },
+ { lineHeight: 1.6, pixels: 9, htmlFontSize: 15 },
+ { lineHeight: 1.0, pixels: 3, htmlFontSize: 14 },
+ ];
+
+ tests.forEach(test => {
+ const { lineHeight, pixels, htmlFontSize } = test;
+
+ describe(`when ${lineHeight} lineHeight, ${pixels} pixels,
+ ${htmlFontSize} htmlFontSize`, () => {
+ const grid = fontGrid({ lineHeight, pixels, htmlFontSize });
+
+ it(`should return a font grid such that the relative lineHeight is aligned`, () => {
+ const absoluteLineHeight = grid * lineHeight * htmlFontSize;
+ assert.strictEqual(Math.round((absoluteLineHeight % pixels) * 100000) / 100000, 0);
+ });
+ });
+
+ it(`with ${lineHeight} lineHeight, ${pixels} pixels,
+ ${htmlFontSize} htmlFontSize, the font grid is such that
+ there is no smaller font aligning the lineHeight`, () => {
+ const grid = fontGrid({ lineHeight, pixels, htmlFontSize });
+ const absoluteLineHeight = grid * lineHeight * htmlFontSize;
+ assert.strictEqual(Math.floor(absoluteLineHeight / pixels), 1);
+ });
+ });
+ });
+
+ describe('responsiveProperty', () => {
+ describe('when providing two breakpoints and pixel units', () => {
+ it('should respond with three styles in pixels', () => {
+ const result = responsiveProperty({
+ cssProperty: 'fontSize',
+ min: 15,
+ max: 20,
+ unit: 'px',
+ breakpoints: [300, 600],
+ });
+
+ assert.deepEqual(result, {
+ fontSize: '15px',
+ '@media (min-width:300px)': {
+ fontSize: '17.5px',
+ },
+ '@media (min-width:600px)': {
+ fontSize: '20px',
+ },
+ });
+ });
+ });
+
+ describe('when providing one breakpoint and requesting rem units', () => {
+ it('should respond with two styles in rem', () => {
+ const result = responsiveProperty({
+ cssProperty: 'fontSize',
+ min: 0.875,
+ max: 1,
+ unit: 'rem',
+ breakpoints: [500],
+ });
+
+ assert.deepEqual(result, {
+ fontSize: '0.875rem',
+ '@media (min-width:500px)': {
+ fontSize: '1rem',
+ },
+ });
+ });
+ });
+ });
+});
diff --git a/packages/material-ui/src/styles/index.js b/packages/material-ui/src/styles/index.js
index 426aacd6007aaa..7c1db851b0f155 100644
--- a/packages/material-ui/src/styles/index.js
+++ b/packages/material-ui/src/styles/index.js
@@ -3,6 +3,7 @@ export { default as createMuiTheme } from './createMuiTheme';
export { default as createStyles } from './createStyles';
export { default as makeStyles } from './makeStyles';
export { default as MuiThemeProvider } from './MuiThemeProvider';
+export { default as responsiveFontSizes } from './responsiveFontSizes';
export { default as styled } from './styled';
export * from './transitions';
export { default as useTheme } from './useTheme';
diff --git a/packages/material-ui/src/styles/responsiveFontSizes.js b/packages/material-ui/src/styles/responsiveFontSizes.js
new file mode 100644
index 00000000000000..25e972f00021c6
--- /dev/null
+++ b/packages/material-ui/src/styles/responsiveFontSizes.js
@@ -0,0 +1,90 @@
+import convertLength from 'convert-css-length';
+import { responsiveProperty, alignProperty, fontGrid } from './cssUtils';
+
+function isUnitless(value) {
+ return String(parseFloat(value)).length === String(value).length;
+}
+
+export default function responsiveFontSizes(themeInput, options = {}) {
+ const {
+ breakpoints = ['sm', 'md', 'lg'],
+ disableAlign = false,
+ factor = 2,
+ variants = [
+ 'h1',
+ 'h2',
+ 'h3',
+ 'h4',
+ 'h5',
+ 'h6',
+ 'subtitle1',
+ 'subtitle2',
+ 'body1',
+ 'body2',
+ 'caption',
+ 'button',
+ 'overline',
+ ],
+ } = options;
+
+ const theme = { ...themeInput };
+ theme.typography = { ...theme.typography };
+ const typography = theme.typography;
+
+ // Convert between css lengths e.g. em->px or px->rem
+ // Set the baseFontSize for your project. Defaults to 16px (also the browser default).
+ const convert = convertLength(typography.htmlFontSize);
+ const breakpointValues = breakpoints.map(x => theme.breakpoints.values[x]);
+
+ variants.forEach(variant => {
+ const style = typography[variant];
+ const remFontSize = parseFloat(convert(style.fontSize, 'rem'));
+
+ if (remFontSize <= 1) {
+ return;
+ }
+
+ const maxFontSize = remFontSize;
+ const minFontSize = 1 + (maxFontSize - 1) / factor;
+
+ let { lineHeight } = style;
+
+ if (!isUnitless(lineHeight) && !disableAlign) {
+ throw new Error(
+ [
+ `Material-UI: unsupported non-unitless line height with grid alignment.`,
+ 'Use unitless line heights instead.',
+ ].join('\n'),
+ );
+ }
+
+ if (!isUnitless(lineHeight)) {
+ // make it unitless
+ lineHeight = parseFloat(convert(lineHeight, 'rem')) / parseFloat(remFontSize);
+ }
+
+ let transform = null;
+
+ if (!disableAlign) {
+ transform = value =>
+ alignProperty({
+ size: value,
+ grid: fontGrid({ pixels: 4, lineHeight, htmlFontSize: typography.htmlFontSize }),
+ });
+ }
+
+ typography[variant] = {
+ ...style,
+ ...responsiveProperty({
+ cssProperty: 'fontSize',
+ min: minFontSize,
+ max: maxFontSize,
+ unit: 'rem',
+ breakpoints: breakpointValues,
+ transform,
+ }),
+ };
+ });
+
+ return theme;
+}
diff --git a/packages/material-ui/src/styles/responsiveFontSizes.test.js b/packages/material-ui/src/styles/responsiveFontSizes.test.js
new file mode 100644
index 00000000000000..4a5b4642949309
--- /dev/null
+++ b/packages/material-ui/src/styles/responsiveFontSizes.test.js
@@ -0,0 +1,72 @@
+import { assert } from 'chai';
+import responsiveFontSizes from './responsiveFontSizes';
+import { createMuiTheme } from '@material-ui/core/styles';
+
+describe('responsiveFontSizes', () => {
+ it('should support unitless line height', () => {
+ const defaultVariant = {
+ fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
+ fontSize: '6rem',
+ fontWeight: 300,
+ letterSpacing: '-0.01562em',
+ lineHeight: 1,
+ };
+
+ const theme = createMuiTheme({
+ typography: {
+ h1: defaultVariant,
+ },
+ });
+ const { typography } = responsiveFontSizes(theme);
+ assert.deepEqual(typography.h1, {
+ ...defaultVariant,
+ fontSize: '3.5rem',
+ '@media (min-width:600px)': { fontSize: '4.75rem' },
+ '@media (min-width:960px)': { fontSize: '5.5rem' },
+ '@media (min-width:1280px)': { fontSize: defaultVariant.fontSize },
+ });
+ });
+
+ it('should disable vertical alignment', () => {
+ const defaultVariant = {
+ fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
+ fontSize: '6rem',
+ fontWeight: 300,
+ letterSpacing: '-0.01562em',
+ lineHeight: '6rem',
+ };
+
+ const theme = createMuiTheme({
+ typography: {
+ h1: defaultVariant,
+ },
+ });
+ const { typography } = responsiveFontSizes(theme, {
+ disableAlign: true,
+ });
+
+ assert.deepEqual(typography.h1, {
+ ...defaultVariant,
+ fontSize: '3.5rem',
+ '@media (min-width:600px)': { fontSize: '4.6719rem' },
+ '@media (min-width:960px)': { fontSize: '5.375rem' },
+ '@media (min-width:1280px)': { fontSize: defaultVariant.fontSize },
+ });
+ });
+
+ describe('when requesting a responsive typography with non unitless line height and alignment', () => {
+ it('should throw an error, as this is not supported', () => {
+ const theme = createMuiTheme({
+ typography: {
+ h1: {
+ lineHeight: '6rem',
+ },
+ },
+ });
+
+ assert.throw(() => {
+ responsiveFontSizes(theme);
+ });
+ });
+ });
+});
diff --git a/yarn.lock b/yarn.lock
index b8902127c804be..411f34f3de766c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1721,14 +1721,15 @@
write-file-atomic "^2.3.0"
"@material-ui/core@^3.9.3":
- version "4.0.0-alpha.8"
+ version "4.0.0-beta.0"
dependencies:
"@babel/runtime" "^7.2.0"
- "@material-ui/styles" "^4.0.0-alpha.8"
- "@material-ui/system" "^4.0.0-alpha.8"
- "@material-ui/utils" "^4.0.0-alpha.8"
+ "@material-ui/styles" "^4.0.0-beta.0"
+ "@material-ui/system" "^4.0.0-beta.0"
+ "@material-ui/utils" "^4.0.0-beta.0"
"@types/react-transition-group" "^2.0.16"
clsx "^1.0.2"
+ convert-css-length "^1.0.2"
csstype "^2.5.2"
debounce "^1.1.0"
deepmerge "^3.0.0"
@@ -4076,6 +4077,11 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0:
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=
+console-polyfill@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/console-polyfill/-/console-polyfill-0.1.2.tgz#96cfed51caf78189f699572e6f18271dc37c0e30"
+ integrity sha1-ls/tUcr3gYn2mVcubxgnHcN8DjA=
+
constants-browserify@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
@@ -4179,6 +4185,14 @@ conventional-recommended-bump@^4.0.4:
meow "^4.0.0"
q "^1.5.1"
+convert-css-length@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/convert-css-length/-/convert-css-length-1.0.2.tgz#32f38a8ac55d78372ff43562532564366c871ccc"
+ integrity sha512-ecV7j3hXyXN1X2XfJBzhMR0o1Obv0v3nHmn0UiS3ACENrzbxE/EknkiunS/fCwQva0U62X1GChi8GaPh4oTlLg==
+ dependencies:
+ console-polyfill "^0.1.2"
+ parse-unit "^1.0.1"
+
convert-source-map@1.6.0, convert-source-map@^1.1.0, convert-source-map@^1.5.0, convert-source-map@^1.5.1, convert-source-map@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20"
@@ -10148,6 +10162,11 @@ parse-path@^4.0.0:
is-ssh "^1.3.0"
protocols "^1.4.0"
+parse-unit@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/parse-unit/-/parse-unit-1.0.1.tgz#7e1bb6d5bef3874c28e392526a2541170291eecf"
+ integrity sha1-fhu21b7zh0wo45JSaiVBFwKR7s8=
+
parse-url@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/parse-url/-/parse-url-5.0.1.tgz#99c4084fc11be14141efa41b3d117a96fcb9527f"