From 39c900de23c25135a90c8a49d0d5b3efb392de6f Mon Sep 17 00:00:00 2001 From: Oleksandr Zavarzin Date: Mon, 24 Aug 2026 12:24:28 +0200 Subject: [PATCH] fix: align typography system with Material Design 3 updates --- docs/6.x/docs/guides/fonts.md | 24 +++++++- src/components/Typography/Text.tsx | 3 + src/theme/__tests__/fonts.test.js | 34 ++++++++++++ src/theme/tokens/ref/typeface.ts | 12 ++++ src/theme/tokens/sys/typography.ts | 88 ++++++++++++++++-------------- src/theme/types/typography.ts | 17 +++++- 6 files changed, 135 insertions(+), 43 deletions(-) diff --git a/docs/6.x/docs/guides/fonts.md b/docs/6.x/docs/guides/fonts.md index 94a8eb40fe..cac73ee41e 100644 --- a/docs/6.x/docs/guides/fonts.md +++ b/docs/6.x/docs/guides/fonts.md @@ -67,6 +67,11 @@ Platform.select({ }), ``` +Material Design 3 splits the typescale across two font families: Brand (Display, +Headline and Title Large) and Plain (Title Medium/Small, Label and Body). Both +resolve to the same platform default above, so they are interchangeable until you +override one of them. + ::: - #### Display @@ -291,7 +296,7 @@ Platform.select({ "fontFamily": "Font", "fontSize": 16, "fontWeight": "400", - "letterSpacing": 0.15, + "letterSpacing": 0.5, "lineHeight": 24, } ``` @@ -299,6 +304,23 @@ Platform.select({ +- #### Emphasized + +Each of the 15 variants above has an `Emphasized` counterpart — `displayLargeEmphasized`, +`bodyMediumEmphasized`, and so on. They keep the size, line height and letter spacing of +their baseline variant and only raise the weight: Display, Headline, Title Large and Body +go to `"500"`, while Title Medium/Small and Label go to `"700"`. + +```json +"bodyMediumEmphasized": { + "fontFamily": "Font", + "fontSize": 14, + "fontWeight": "500", + "letterSpacing": 0.25, + "lineHeight": 20, +} +``` + :::info If any component uses Paper's `Text` component, without specified variant, then `default` variant is applied: diff --git a/src/components/Typography/Text.tsx b/src/components/Typography/Text.tsx index 38a14fb423..5beeed1630 100644 --- a/src/components/Typography/Text.tsx +++ b/src/components/Typography/Text.tsx @@ -25,6 +25,9 @@ export type Props = React.ComponentProps & { * Label: `labelLarge`, `labelMedium`, `labelSmall` * * Body: `bodyLarge`, `bodyMedium`, `bodySmall` + * + * Every variant also has an `Emphasized` counterpart, e.g. `displayLargeEmphasized` + * or `bodyMediumEmphasized`, which renders the same size at a heavier weight. */ variant?: VariantProp; children: React.ReactNode; diff --git a/src/theme/__tests__/fonts.test.js b/src/theme/__tests__/fonts.test.js index 23e0f794ae..3c200412be 100644 --- a/src/theme/__tests__/fonts.test.js +++ b/src/theme/__tests__/fonts.test.js @@ -340,6 +340,40 @@ describe('configureFonts', () => { }); }); + it('resolves the Android font families per MD3 family assignment', () => { + mockPlatform('android'); + const { typescale } = loadFonts(); + + for (const variant of ['bodyLarge', 'bodyMedium', 'bodySmall']) { + expect(typescale[variant]).toMatchObject({ + fontFamily: 'sans-serif', + fontWeight: '400', + }); + } + + for (const variant of [ + 'displayLargeEmphasized', + 'displayMediumEmphasized', + 'displaySmallEmphasized', + 'headlineLargeEmphasized', + 'headlineMediumEmphasized', + 'headlineSmallEmphasized', + 'titleLargeEmphasized', + ]) { + expect(typescale[variant]).toMatchObject({ + fontFamily: 'sans-serif-medium', + fontWeight: '500', + }); + } + + for (const variant of ['displayLarge', 'headlineLarge', 'titleLarge']) { + expect(typescale[variant]).toMatchObject({ + fontFamily: 'sans-serif', + fontWeight: '400', + }); + } + }); + it('should be deterministic', () => { mockPlatform('ios'); const { configureFonts } = loadFonts(); diff --git a/src/theme/tokens/ref/typeface.ts b/src/theme/tokens/ref/typeface.ts index fdf6993ae8..f62389b09b 100644 --- a/src/theme/tokens/ref/typeface.ts +++ b/src/theme/tokens/ref/typeface.ts @@ -9,8 +9,18 @@ export const typeface = { ios: 'System', default: 'sans-serif', }), + brandMedium: Platform.select({ + web: 'Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif', + ios: 'System', + default: 'sans-serif-medium', + }), weightRegular: '400', + plainRegular: Platform.select({ + web: 'Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif', + ios: 'System', + default: 'sans-serif', + }), plainMedium: Platform.select({ web: 'Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif', ios: 'System', @@ -21,7 +31,9 @@ export const typeface = { weightBold: '700', } satisfies { brandRegular?: string; + brandMedium?: string; weightRegular: Font['fontWeight']; + plainRegular?: string; plainMedium?: string; weightMedium: Font['fontWeight']; weightBold: Font['fontWeight']; diff --git a/src/theme/tokens/sys/typography.ts b/src/theme/tokens/sys/typography.ts index 289cbdedef..98177a5c8e 100644 --- a/src/theme/tokens/sys/typography.ts +++ b/src/theme/tokens/sys/typography.ts @@ -1,25 +1,37 @@ import type { Typescale } from '../../types'; import { typeface } from '../ref/typeface'; -const regularType = { +const brandRegularType = { fontFamily: typeface.brandRegular, letterSpacing: 0, fontWeight: typeface.weightRegular, }; -const mediumType = { +const brandMediumType = { + fontFamily: typeface.brandMedium, + letterSpacing: 0, + fontWeight: typeface.weightMedium, +}; + +const plainRegularType = { + fontFamily: typeface.plainRegular, + letterSpacing: 0, + fontWeight: typeface.weightRegular, +}; + +const plainMediumType = { fontFamily: typeface.plainMedium, letterSpacing: 0.15, fontWeight: typeface.weightMedium, }; -const emphasizedMediumType = { +const plainMediumEmphasizedType = { fontFamily: typeface.plainMedium, letterSpacing: 0, fontWeight: typeface.weightMedium, }; -const emphasizedBoldType = { +const plainBoldType = { fontFamily: typeface.plainMedium, letterSpacing: 0, fontWeight: typeface.weightBold, @@ -28,190 +40,184 @@ const emphasizedBoldType = { /** md.sys.typescale.* */ export const typescale = { displayLarge: { - ...regularType, + ...brandRegularType, letterSpacing: -0.25, lineHeight: 64, fontSize: 57, }, displayMedium: { - ...regularType, + ...brandRegularType, lineHeight: 52, fontSize: 45, }, displaySmall: { - ...regularType, + ...brandRegularType, lineHeight: 44, fontSize: 36, }, headlineLarge: { - ...regularType, + ...brandRegularType, lineHeight: 40, fontSize: 32, }, headlineMedium: { - ...regularType, + ...brandRegularType, lineHeight: 36, fontSize: 28, }, headlineSmall: { - ...regularType, + ...brandRegularType, lineHeight: 32, fontSize: 24, }, titleLarge: { - ...regularType, + ...brandRegularType, lineHeight: 28, fontSize: 22, }, titleMedium: { - ...mediumType, + ...plainMediumType, lineHeight: 24, fontSize: 16, }, titleSmall: { - ...mediumType, + ...plainMediumType, letterSpacing: 0.1, lineHeight: 20, fontSize: 14, }, labelLarge: { - ...mediumType, + ...plainMediumType, letterSpacing: 0.1, lineHeight: 20, fontSize: 14, }, labelMedium: { - ...mediumType, + ...plainMediumType, letterSpacing: 0.5, lineHeight: 16, fontSize: 12, }, labelSmall: { - ...mediumType, + ...plainMediumType, letterSpacing: 0.5, lineHeight: 16, fontSize: 11, }, bodyLarge: { - ...mediumType, - fontWeight: typeface.weightRegular, - fontFamily: typeface.brandRegular, + ...plainRegularType, letterSpacing: 0.5, lineHeight: 24, fontSize: 16, }, bodyMedium: { - ...mediumType, - fontWeight: typeface.weightRegular, - fontFamily: typeface.brandRegular, + ...plainRegularType, letterSpacing: 0.25, lineHeight: 20, fontSize: 14, }, bodySmall: { - ...mediumType, - fontWeight: typeface.weightRegular, - fontFamily: typeface.brandRegular, + ...plainRegularType, letterSpacing: 0.4, lineHeight: 16, fontSize: 12, }, displayLargeEmphasized: { - ...emphasizedMediumType, + ...brandMediumType, letterSpacing: -0.25, lineHeight: 64, fontSize: 57, }, displayMediumEmphasized: { - ...emphasizedMediumType, + ...brandMediumType, lineHeight: 52, fontSize: 45, }, displaySmallEmphasized: { - ...emphasizedMediumType, + ...brandMediumType, lineHeight: 44, fontSize: 36, }, headlineLargeEmphasized: { - ...emphasizedMediumType, + ...brandMediumType, lineHeight: 40, fontSize: 32, }, headlineMediumEmphasized: { - ...emphasizedMediumType, + ...brandMediumType, lineHeight: 36, fontSize: 28, }, headlineSmallEmphasized: { - ...emphasizedMediumType, + ...brandMediumType, lineHeight: 32, fontSize: 24, }, titleLargeEmphasized: { - ...emphasizedMediumType, + ...brandMediumType, lineHeight: 28, fontSize: 22, }, titleMediumEmphasized: { - ...emphasizedBoldType, + ...plainBoldType, letterSpacing: 0.15, lineHeight: 24, fontSize: 16, }, titleSmallEmphasized: { - ...emphasizedBoldType, + ...plainBoldType, letterSpacing: 0.1, lineHeight: 20, fontSize: 14, }, labelLargeEmphasized: { - ...emphasizedBoldType, + ...plainBoldType, letterSpacing: 0.1, lineHeight: 20, fontSize: 14, }, labelMediumEmphasized: { - ...emphasizedBoldType, + ...plainBoldType, letterSpacing: 0.5, lineHeight: 16, fontSize: 12, }, labelSmallEmphasized: { - ...emphasizedBoldType, + ...plainBoldType, letterSpacing: 0.5, lineHeight: 16, fontSize: 11, }, bodyLargeEmphasized: { - ...emphasizedMediumType, + ...plainMediumEmphasizedType, letterSpacing: 0.5, lineHeight: 24, fontSize: 16, }, bodyMediumEmphasized: { - ...emphasizedMediumType, + ...plainMediumEmphasizedType, letterSpacing: 0.25, lineHeight: 20, fontSize: 14, }, bodySmallEmphasized: { - ...emphasizedMediumType, + ...plainMediumEmphasizedType, letterSpacing: 0.4, lineHeight: 16, fontSize: 12, }, default: { - ...regularType, + ...brandRegularType, }, }; diff --git a/src/theme/types/typography.ts b/src/theme/types/typography.ts index 57fcf3f67e..f7ad73b80f 100644 --- a/src/theme/types/typography.ts +++ b/src/theme/types/typography.ts @@ -37,7 +37,22 @@ export type TypescaleKey = | 'labelSmall' | 'bodyLarge' | 'bodyMedium' - | 'bodySmall'; + | 'bodySmall' + | 'displayLargeEmphasized' + | 'displayMediumEmphasized' + | 'displaySmallEmphasized' + | 'headlineLargeEmphasized' + | 'headlineMediumEmphasized' + | 'headlineSmallEmphasized' + | 'titleLargeEmphasized' + | 'titleMediumEmphasized' + | 'titleSmallEmphasized' + | 'labelLargeEmphasized' + | 'labelMediumEmphasized' + | 'labelSmallEmphasized' + | 'bodyLargeEmphasized' + | 'bodyMediumEmphasized' + | 'bodySmallEmphasized'; export type TypescaleStyle = { fontFamily: string;