feat(themes): add md theme tokens and update ionic theme to include semantic tokens (#30734)

- Updates the theme tokens interface to add more numeric tokens & semantic tokens
- Moves the `config` values into a separate `config` option which uses the `IonicConfig` interface
  - Adds a config option for `formHighlight` to `IonicConfig`
- Defines default & dark tokens for the `md` theme
- Removes the numeric tokens from the `ionic` theme and adds semantic tokens
- Remove the numerous `--ion-font-family` overrides in favor of the tokens

---------

Co-authored-by: Brandy Smith <6577830+brandyscarney@users.noreply.github.com>
This commit is contained in:
Brandy Smith
2025-10-21 15:56:18 -04:00
committed by GitHub
parent 3fd1d5cab1
commit a7699eabbe
12 changed files with 276 additions and 121 deletions

File diff suppressed because one or more lines are too long

View File

@ -7,16 +7,9 @@
// Ionic Font Family // Ionic Font Family
// -------------------------------------------------- // --------------------------------------------------
// TODO(FW-6744): Remove this after adding the ios tokens
html.ios { html.ios {
--ion-default-font: -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Roboto", sans-serif; --ion-font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Roboto", sans-serif;
}
html.md {
--ion-default-font: "Roboto", "Helvetica Neue", sans-serif;
}
html {
--ion-dynamic-font: -apple-system-body;
--ion-font-family: var(--ion-default-font);
} }
body { body {

View File

@ -8,10 +8,6 @@
--background: #{globals.$ion-bg-surface-default}; --background: #{globals.$ion-bg-surface-default};
} }
html {
--ionic-dynamic-font: -apple-system-body;
}
body { body {
background: var(--ion-background-color, #{globals.$ion-bg-body}); background: var(--ion-background-color, #{globals.$ion-bg-body});
color: var(--ion-text-color, #{globals.$ion-text-default}); color: var(--ion-text-color, #{globals.$ion-text-default});

View File

@ -2,7 +2,7 @@
// TODO(ROU-10833): add font loading solution here, as a @font-face, base64 or cdn // TODO(ROU-10833): add font loading solution here, as a @font-face, base64 or cdn
html { html {
font-family: globals.$ion-font-family; font-family: var(--ion-font-family);
} }
body { body {

View File

@ -11,12 +11,16 @@ export const defaultTheme: DefaultTheme = {
dark: darkTheme, dark: darkTheme,
}, },
formHighlight: false, config: {
rippleEffect: false, formHighlight: false,
rippleEffect: false,
},
spacing: { spacing: {
0: '0px', 0: '0px',
25: '1px',
50: '2px', 50: '2px',
75: '3px',
100: '4px', 100: '4px',
150: '6px', 150: '6px',
200: '8px', 200: '8px',
@ -98,17 +102,26 @@ export const defaultTheme: DefaultTheme = {
300: '12px', 300: '12px',
350: '14px', 350: '14px',
400: '16px', 400: '16px',
hairline: '0.55px',
}, },
radii: { radii: {
0: '0px', 0: '0px',
25: '2px', 25: '1px',
50: '2px',
75: '3px',
100: '4px', 100: '4px',
150: '6px',
200: '8px', 200: '8px',
250: '10px',
300: '12px', 300: '12px',
350: '14px',
400: '16px', 400: '16px',
500: '20px', 500: '20px',
600: '24px',
700: '28px',
800: '32px', 800: '32px',
900: '36px',
1000: '40px', 1000: '40px',
full: '999px', full: '999px',
}, },

View File

@ -14,104 +14,66 @@ export const defaultTheme: DefaultTheme = {
dark: darkTheme, dark: darkTheme,
}, },
formHighlight: false, config: {
rippleEffect: false, formHighlight: true,
},
// TODO(FW-6745): see if we can remove this after the md tokens are added fontFamily:
fontFamily: 'initial', '-apple-system, system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
spacing: { spacing: {
0: '0px', xxxxs: 'var(--ion-spacing-25)',
50: '2px', xxxs: 'var(--ion-spacing-50)',
100: '4px', xxs: 'var(--ion-spacing-100)',
150: '6px', xs: 'var(--ion-spacing-150)',
200: '8px', sm: 'var(--ion-spacing-200)',
250: '10px', md: 'var(--ion-spacing-300)',
300: '12px', lg: 'var(--ion-spacing-400)',
350: '14px', xl: 'var(--ion-spacing-500)',
400: '16px', xxl: 'var(--ion-spacing-600)',
450: '18px', xxxl: 'var(--ion-spacing-700)',
500: '20px', xxxxl: 'var(--ion-spacing-800)',
550: '22px',
600: '24px',
650: '26px',
700: '28px',
750: '30px',
800: '32px',
850: '34px',
900: '36px',
950: '38px',
1000: '40px',
1050: '42px',
1100: '44px',
1150: '46px',
1200: '48px',
}, },
scaling: { scaling: {
0: '0px', xxxxs: 'var(--ion-scaling-300)',
25: '1px', xxxs: 'var(--ion-scaling-400)',
50: '2px', xxs: 'var(--ion-scaling-500)',
75: '3px', xs: 'var(--ion-scaling-600)',
100: '4px', sm: 'var(--ion-scaling-700)',
150: '6px', md: 'var(--ion-scaling-800)',
200: '8px', lg: 'var(--ion-scaling-1000)',
250: '10px', xl: 'var(--ion-scaling-1200)',
300: '12px', xxl: 'var(--ion-scaling-1400)',
350: '14px', xxxl: 'var(--ion-scaling-1800)',
400: '16px', xxxxl: 'var(--ion-scaling-2000)',
450: '18px',
500: '20px',
550: '22px',
600: '24px',
650: '26px',
700: '28px',
750: '30px',
800: '32px',
850: '34px',
900: '36px',
950: '38px',
1000: '40px',
1050: '42px',
1100: '44px',
1150: '46px',
1200: '48px',
1400: '56px',
1600: '64px',
1800: '72px',
2000: '80px',
2400: '96px',
2800: '112px',
3200: '128px',
3400: '136px',
3600: '144px',
4000: '160px',
5000: '200px',
6200: '248px',
7400: '296px',
9000: '360px',
},
radii: {
0: '0px',
25: '2px',
100: '4px',
200: '8px',
300: '12px',
400: '16px',
500: '20px',
800: '32px',
1000: '40px',
full: '999px',
}, },
borderWidth: { borderWidth: {
0: '0', xxxxs: 'var(--ion-border-width-0)',
25: '1px', xxxs: 'var(--ion-border-width-25)',
50: '2px', xxs: 'var(--ion-border-width-50)',
75: '3px', xs: 'var(--ion-border-width-75)',
100: '4px', sm: 'var(--ion-border-width-100)',
150: '6px', md: 'var(--ion-border-width-150)',
200: '8px', lg: 'var(--ion-border-width-200)',
xl: 'var(--ion-border-width-250)',
xxl: 'var(--ion-border-width-300)',
xxxl: 'var(--ion-border-width-350)',
xxxxl: 'var(--ion-border-width-400)',
},
radii: {
xxxxs: 'var(--ion-radii-0)',
xxxs: 'var(--ion-radii-25)',
xxs: 'var(--ion-radii-50)',
xs: 'var(--ion-radii-75)',
sm: 'var(--ion-radii-100)',
md: 'var(--ion-radii-200)',
lg: 'var(--ion-radii-300)',
xl: 'var(--ion-radii-400)',
xxl: 'var(--ion-radii-500)',
xxxl: 'var(--ion-radii-1000)',
xxxxl: 'var(--ion-radii-full)',
}, },
}; };

View File

@ -3,4 +3,68 @@ import type { DarkTheme } from '../themes.interfaces';
export const darkTheme: DarkTheme = { export const darkTheme: DarkTheme = {
...baseDarkTheme, ...baseDarkTheme,
backgroundColor: '#121212',
backgroundColorRgb: '18, 18, 18',
textColor: '#ffffff',
textColorRgb: '255, 255, 255',
backgroundColorStep: {
50: '#1e1e1e',
100: '#2a2a2a',
150: '#363636',
200: '#414141',
250: '#4d4d4d',
300: '#595959',
350: '#656565',
400: '#717171',
450: '#7d7d7d',
500: '#898989',
550: '#949494',
600: '#a0a0a0',
650: '#acacac',
700: '#b8b8b8',
750: '#c4c4c4',
800: '#d0d0d0',
850: '#dbdbdb',
900: '#e7e7e7',
950: '#f3f3f3',
},
textColorStep: {
50: '#f3f3f3',
100: '#e7e7e7',
150: '#dbdbdb',
200: '#d0d0d0',
250: '#c4c4c4',
300: '#b8b8b8',
350: '#acacac',
400: '#a0a0a0',
450: '#949494',
500: '#898989',
550: '#7d7d7d',
600: '#717171',
650: '#656565',
700: '#595959',
750: '#4d4d4d',
800: '#414141',
850: '#363636',
900: '#2a2a2a',
950: '#1e1e1e',
},
components: {
IonCard: {
background: '#1e1e1e',
},
IonItem: {
background: '#1e1e1e',
},
IonToolbar: {
background: '#1f1f1f',
},
IonTabBar: {
background: '#1f1f1f',
},
},
}; };

View File

@ -13,4 +13,67 @@ export const defaultTheme: DefaultTheme = {
light: lightTheme, light: lightTheme,
dark: darkTheme, dark: darkTheme,
}, },
config: {
formHighlight: true,
rippleEffect: true,
},
fontFamily: '"Roboto", "Helvetica Neue", sans-serif',
spacing: {
xxxxs: 'var(--ion-spacing-25)',
xxxs: 'var(--ion-spacing-50)',
xxs: 'var(--ion-spacing-100)',
xs: 'var(--ion-spacing-150)',
sm: 'var(--ion-spacing-200)',
md: 'var(--ion-spacing-300)',
lg: 'var(--ion-spacing-400)',
xl: 'var(--ion-spacing-500)',
xxl: 'var(--ion-spacing-600)',
xxxl: 'var(--ion-spacing-700)',
xxxxl: 'var(--ion-spacing-800)',
},
scaling: {
xxxxs: 'var(--ion-scaling-300)',
xxxs: 'var(--ion-scaling-400)',
xxs: 'var(--ion-scaling-500)',
xs: 'var(--ion-scaling-600)',
sm: 'var(--ion-scaling-700)',
md: 'var(--ion-scaling-800)',
lg: 'var(--ion-scaling-900)',
xl: 'var(--ion-scaling-1000)',
xxl: 'var(--ion-scaling-1100)',
xxxl: 'var(--ion-scaling-1200)',
xxxxl: 'var(--ion-scaling-1600)',
},
borderWidth: {
xxxxs: 'var(--ion-border-width-0)',
xxxs: 'var(--ion-border-width-25)',
xxs: 'var(--ion-border-width-50)',
xs: 'var(--ion-border-width-75)',
sm: 'var(--ion-border-width-100)',
md: 'var(--ion-border-width-150)',
lg: 'var(--ion-border-width-200)',
xl: 'var(--ion-border-width-250)',
xxl: 'var(--ion-border-width-300)',
xxxl: 'var(--ion-border-width-350)',
xxxxl: 'var(--ion-border-width-400)',
},
radii: {
xxxxs: 'var(--ion-radii-0)',
xxxs: 'var(--ion-radii-25)',
xxs: 'var(--ion-radii-50)',
xs: 'var(--ion-radii-75)',
sm: 'var(--ion-radii-100)',
md: 'var(--ion-radii-200)',
lg: 'var(--ion-radii-300)',
xl: 'var(--ion-radii-400)',
xxl: 'var(--ion-radii-700)',
xxxl: 'var(--ion-radii-900)',
xxxxl: 'var(--ion-radii-full)',
},
}; };

View File

@ -1,12 +1,10 @@
import type { IonicConfig } from '../utils/config';
// Platform-specific theme // Platform-specific theme
export type PlatformTheme = Omit<BaseTheme, 'ios' | 'md'>; export type PlatformTheme = Omit<BaseTheme, 'ios' | 'md'>;
// Base tokens for all palettes // Base tokens for all palettes
export type BaseTheme = { export type BaseTheme = {
// CONFIG TOKENS
rippleEffect?: boolean;
formHighlight?: boolean;
// GLOBAL THEME TOKENS // GLOBAL THEME TOKENS
backgroundColor?: string; backgroundColor?: string;
backgroundColorRgb?: string; backgroundColorRgb?: string;
@ -22,7 +20,9 @@ export type BaseTheme = {
// SPACE TOKENS // SPACE TOKENS
spacing?: { spacing?: {
0?: string; 0?: string;
25?: string;
50?: string; 50?: string;
75?: string;
100?: string; 100?: string;
150?: string; 150?: string;
200?: string; 200?: string;
@ -46,6 +46,17 @@ export type BaseTheme = {
1100?: string; 1100?: string;
1150?: string; 1150?: string;
1200?: string; 1200?: string;
xxxxs?: string;
xxxs?: string;
xxs?: string;
xs?: string;
sm?: string;
md?: string;
lg?: string;
xl?: string;
xxl?: string;
xxxl?: string;
xxxxl?: string;
}; };
scaling?: { scaling?: {
@ -90,6 +101,17 @@ export type BaseTheme = {
6200?: string; 6200?: string;
7400?: string; 7400?: string;
9000?: string; 9000?: string;
xxxxs?: string;
xxxs?: string;
xxs?: string;
xs?: string;
sm?: string;
md?: string;
lg?: string;
xl?: string;
xxl?: string;
xxxl?: string;
xxxxl?: string;
}; };
// APPEARANCE TOKENS // APPEARANCE TOKENS
@ -105,18 +127,49 @@ export type BaseTheme = {
300?: string; 300?: string;
350?: string; 350?: string;
400?: string; 400?: string;
xxxxs?: string;
xxxs?: string;
xxs?: string;
xs?: string;
sm?: string;
md?: string;
lg?: string;
xl?: string;
xxl?: string;
xxxl?: string;
xxxxl?: string;
hairline?: string;
}; };
radii?: { radii?: {
0?: string; 0?: string;
25?: string; 25?: string;
50?: string;
75?: string;
100?: string; 100?: string;
150?: string;
200?: string; 200?: string;
250?: string;
300?: string; 300?: string;
350?: string;
400?: string; 400?: string;
500?: string; 500?: string;
600?: string;
700?: string;
800?: string; 800?: string;
900?: string;
1000?: string; 1000?: string;
xxxxs?: string;
xxxs?: string;
xxs?: string;
xs?: string;
sm?: string;
md?: string;
lg?: string;
xl?: string;
xxl?: string;
xxxl?: string;
xxxxl?: string;
full?: string; full?: string;
}; };
@ -205,4 +258,6 @@ export type DefaultTheme = BaseTheme & {
light?: LightTheme; light?: LightTheme;
dark?: DarkTheme; dark?: DarkTheme;
}; };
config?: IonicConfig;
}; };

View File

@ -12,6 +12,12 @@ export interface IonicConfig {
*/ */
animated?: boolean; animated?: boolean;
/**
* When it's set to `false`, it disables the form highlight effect across the app.
* Defaults to `false`.
*/
formHighlight?: boolean;
/** /**
* When it's set to `false`, it disables all material-design ripple-effects across the app. * When it's set to `false`, it disables all material-design ripple-effects across the app.
* Defaults to `true`. * Defaults to `true`.

View File

@ -186,6 +186,7 @@ describe('getCustomTheme', () => {
describe('generateCSSVars', () => { describe('generateCSSVars', () => {
it('should not generate CSS variables for an empty theme', () => { it('should not generate CSS variables for an empty theme', () => {
const theme = { const theme = {
name: 'test',
palette: { palette: {
light: {}, light: {},
dark: {}, dark: {},
@ -198,14 +199,17 @@ describe('generateCSSVars', () => {
it('should generate CSS variables for a given theme', () => { it('should generate CSS variables for a given theme', () => {
const theme = { const theme = {
name: 'test',
palette: { palette: {
light: {}, light: {},
dark: { dark: {
enabled: 'system', enabled: 'system',
}, },
}, },
rippleEffect: true, config: {
formHighlight: true, rippleEffect: true,
formHighlight: true,
},
borderWidth: { borderWidth: {
sm: '4px', sm: '4px',
}, },
@ -309,6 +313,7 @@ describe('injectCSS', () => {
describe('generateGlobalThemeCSS', () => { describe('generateGlobalThemeCSS', () => {
it('should generate global CSS for a given theme', () => { it('should generate global CSS for a given theme', () => {
const theme = { const theme = {
name: 'test',
palette: { palette: {
light: {}, light: {},
dark: { dark: {
@ -339,6 +344,7 @@ describe('generateGlobalThemeCSS', () => {
it('should generate global CSS for a given theme with light palette', () => { it('should generate global CSS for a given theme with light palette', () => {
const theme = { const theme = {
name: 'test',
palette: { palette: {
light: { light: {
color: { color: {
@ -425,6 +431,7 @@ describe('generateGlobalThemeCSS', () => {
it('should not include component or palette variables in global CSS', () => { it('should not include component or palette variables in global CSS', () => {
const theme = { const theme = {
name: 'test',
palette: { palette: {
light: {}, light: {},
dark: { dark: {
@ -479,6 +486,7 @@ describe('generateGlobalThemeCSS', () => {
it('should generate global CSS for a given theme with dark palette enabled for system preference', () => { it('should generate global CSS for a given theme with dark palette enabled for system preference', () => {
const theme = { const theme = {
name: 'test',
palette: { palette: {
light: {}, light: {},
dark: { dark: {
@ -598,6 +606,7 @@ describe('generateColorClasses', () => {
it('should generate color classes for a given theme', () => { it('should generate color classes for a given theme', () => {
const theme = { const theme = {
name: 'test',
palette: { palette: {
light: { light: {
color: { color: {

View File

@ -87,7 +87,7 @@ export const generateCSSVars = (theme: any, prefix: string = CSS_PROPS_PREFIX):
} }
// Do not generate CSS variables for excluded keys // Do not generate CSS variables for excluded keys
const excludedKeys = ['enabled', 'ripple-effect', 'form-highlight']; const excludedKeys = ['name', 'enabled', 'config'];
if (excludedKeys.includes(key)) { if (excludedKeys.includes(key)) {
return []; return [];
} }